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

Merge branch 'support/share-link-for-outside-for-merge' into imprv/share-link-for-outer

akira-s 5 лет назад
Родитель
Сommit
d98f25e652
36 измененных файлов с 433 добавлено и 97 удалено
  1. 7 2
      CHANGES.md
  2. 1 1
      package.json
  3. 6 0
      resource/locales/en-US/translation.json
  4. 6 0
      resource/locales/ja/translation.json
  5. 35 11
      src/client/js/components/Page/PageShareManagement.jsx
  6. 2 2
      src/client/js/components/PageEditor.jsx
  7. 8 4
      src/client/js/components/PageEditor/TextAreaEditor.jsx
  8. 7 7
      src/client/js/components/SearchForm.jsx
  9. 3 3
      src/client/js/components/Sidebar/SidebarNav.jsx
  10. 3 0
      src/client/js/services/AppContainer.js
  11. 0 1
      src/client/styles/scss/_layout.scss
  12. 3 0
      src/client/styles/scss/_navbar.scss
  13. 4 10
      src/client/styles/scss/_on-edit.scss
  14. 5 0
      src/client/styles/scss/_override-bootstrap.scss
  15. 57 3
      src/client/styles/scss/_search.scss
  16. 3 0
      src/client/styles/scss/atoms/_nav.scss
  17. 8 0
      src/client/styles/scss/style-app.scss
  18. 0 12
      src/client/styles/scss/theme/_apply-colors-light.scss
  19. 5 0
      src/client/styles/scss/theme/_apply-colors.scss
  20. 1 1
      src/lib/components/PagePathHierarchicalLink.jsx
  21. 1 0
      src/server/models/index.js
  22. 29 0
      src/server/models/share-link.js
  23. 1 0
      src/server/routes/apiv3/index.js
  24. 81 0
      src/server/routes/apiv3/share-links.js
  25. 2 0
      src/server/routes/index.js
  26. 25 0
      src/server/routes/page.js
  27. 8 12
      src/server/views/layout-growi/base/layout.html
  28. 44 0
      src/server/views/layout-growi/shared_page.html
  29. 6 8
      src/server/views/layout-growi/widget/liker-and-seenusers.html
  30. 43 0
      src/server/views/layout-kibela/shared_page.html
  31. 6 5
      src/server/views/layout/layout.html
  32. 3 3
      src/server/views/login/error.html
  33. 1 1
      src/server/views/widget/not_found_tabs.html
  34. 6 6
      src/server/views/widget/page_tabs.html
  35. 1 1
      src/server/views/widget/page_tabs_kibela.html
  36. 12 4
      src/server/views/widget/user_page_content.html

+ 7 - 2
CHANGES.md

@@ -1,9 +1,14 @@
 # CHANGES
 
-## v4.0.2-RC
+## v4.0.3-RC
+
+* Fix: Editor doesn't work on mobile
+* Fix: navbar is broken on Safari
+
+## v4.0.2
 
 * Fix: Internal Server Error occurred when the guest user access to the pages that has likes
-* Fix: Some buttons are broken in Safari
+* Fix: Some buttons are broken on Safari
 
 ## v4.0.1
 

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "4.0.2-RC",
+  "version": "4.0.3-RC",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",

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

@@ -47,6 +47,7 @@
   "History": "History",
   "Presentation Mode": "Presentation",
   "Not available for guest": "Not available for guest",
+  "Shere this page link to public": "Shere this page link to public",
   "username": "Username",
   "Created": "Created",
   "Last updated": "Updated",
@@ -678,5 +679,10 @@
     "export_menu": "Export Menu",
     "download": "Download",
     "delete": "Delete"
+  },
+  "login": {
+    "Sign in error": "Login error",
+    "Registration successful": "Registration successful",
+    "Setup": "Setup"
   }
 }

+ 6 - 0
resource/locales/ja/translation.json

@@ -47,6 +47,7 @@
   "History": "更新履歴",
   "Presentation Mode": "プレゼンテーション",
   "Not available for guest": "ゲストユーザーは利用できません",
+  "Shere this page link to public": "外部に共有するリンクを発行する",
   "username": "ユーザー名",
   "Created": "作成日",
   "Last updated": "最終更新",
@@ -667,5 +668,10 @@
     "export_menu": "エクスポートメニュー",
     "download": "ダウンロード",
     "delete": "削除"
+  },
+  "login": {
+    "Sign in error": "ログインエラー",
+    "Registration successful": "登録完了",
+    "Setup": "セットアップ"
   }
 }

+ 35 - 11
src/client/js/components/Page/PageShareManagement.jsx

@@ -1,5 +1,6 @@
 import React, { useState } from 'react';
 import PropTypes from 'prop-types';
+import { UncontrolledTooltip } from 'reactstrap';
 import { withTranslation } from 'react-i18next';
 
 import { createSubscribedElement } from '../UnstatedUtils';
@@ -34,21 +35,44 @@ const PageShareManagement = (props) => {
     );
   }
 
+  function renderCurrentUser() {
+    return (
+      <>
+        <button
+          type="button"
+          className="nav-link bg-transparent dropdown-toggle dropdown-toggle-no-caret"
+          data-toggle="dropdown"
+        >
+          <i className="icon-share"></i>
+        </button>
+      </>
+    );
+  }
+
+  function renderGuestUser() {
+    return (
+      <>
+        <button
+          type="button"
+          className="nav-link bg-transparent"
+          id="auth-guest-tltips"
+        >
+          <i className="icon-share"></i>
+        </button>
+        <UncontrolledTooltip placement="top" target="auth-guest-tltips">
+          {t('Not available for guest')}
+        </UncontrolledTooltip>
+      </>
+    );
+  }
+
+
   return (
     <>
-      <button
-        type="button"
-        className={`nav-link dropdown-toggle dropdown-toggle-no-caret ${currentUser == null && 'dropdown-toggle-disabled'}`}
-        data-toggle={`${currentUser == null ? 'tooltip' : 'dropdown'}`}
-        data-placement="top"
-        data-container="body"
-        title={t('Not available for guest')}
-      >
-        <i className="fa fa-share-alt"></i>
-      </button>
+      {currentUser == null ? renderGuestUser() : renderCurrentUser()}
       <div className="dropdown-menu dropdown-menu-right">
         <button className="dropdown-item" type="button" onClick={openOutsideShareLinkModalHandler}>
-          <i className="icon-fw icon-magic-wand"></i> { t('template.option_label.create/edit') }
+          <i className="icon-link"></i> {t('Shere this page link to public')}
         </button>
       </div>
       {renderModals()}

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

@@ -289,7 +289,7 @@ class PageEditor extends React.Component {
 
     return (
       <div className="d-flex flex-wrap">
-        <div className="page-editor-editor-container" style={{ flex: 1 }}>
+        <div className="page-editor-editor-container flex-grow-1 flex-basis-0 mw-0">
           <Editor
             ref={(c) => { this.editor = c }}
             value={this.state.markdown}
@@ -305,7 +305,7 @@ class PageEditor extends React.Component {
             onSave={this.onSaveWithShortcut}
           />
         </div>
-        <div className="d-none d-xl-block page-editor-preview-container" style={{ flex: 1 }}>
+        <div className="d-none d-xl-block page-editor-preview-container flex-grow-1 flex-basis-0 mw-0">
           <Preview
             markdown={this.state.markdown}
             // eslint-disable-next-line no-return-assign

+ 8 - 4
src/client/js/components/PageEditor/TextAreaEditor.jsx

@@ -3,6 +3,8 @@ import React from 'react';
 
 import InterceptorManager from '@commons/service/interceptor-manager';
 
+import { Input } from 'reactstrap';
+
 import AbstractEditor from './AbstractEditor';
 
 import pasteHelper from './PasteHelper';
@@ -21,6 +23,8 @@ export default class TextAreaEditor extends AbstractEditor {
       isGfmMode: this.props.isGfmMode,
     };
 
+    this.textarea = React.createRef();
+
     this.init();
 
     this.handleEnterKey = this.handleEnterKey.bind(this);
@@ -249,10 +253,10 @@ export default class TextAreaEditor extends AbstractEditor {
   render() {
     return (
       <React.Fragment>
-        <input
-          componentClass="textarea"
-          className="textarea-editor"
-          inputRef={(ref) => { this.textarea = ref }}
+        <Input
+          type="textarea"
+          className="textarea-editor shadow-none"
+          innerRef={(c) => { this.textarea = c }}
           defaultValue={this.state.value}
           onChange={(e) => {
           if (this.props.onChange != null) {

+ 7 - 7
src/client/js/components/SearchForm.jsx

@@ -62,37 +62,37 @@ class SearchForm extends React.Component {
         </caption>
         <tbody>
           <tr>
-            <th className="text-right py-2">
+            <th className="py-2">
               <code>word1</code> <code>word2</code><br></br>
               <small>({ t('search_help.and.syntax help') })</small>
             </th>
             <td><h6 className="m-0">{ t('search_help.and.desc', { word1: 'word1', word2: 'word2' }) }</h6></td>
           </tr>
           <tr>
-            <th className="text-right py-2">
+            <th className="py-2">
               <code>&quot;This is GROWI&quot;</code><br></br>
               <small>({ t('search_help.phrase.syntax help') })</small>
             </th>
             <td><h6 className="m-0">{ t('search_help.phrase.desc', { phrase: 'This is GROWI' }) }</h6></td>
           </tr>
           <tr>
-            <th className="text-right py-2"><code>-keyword</code></th>
+            <th className="py-2"><code>-keyword</code></th>
             <td><h6 className="m-0">{ t('search_help.exclude.desc', { word: 'keyword' }) }</h6></td>
           </tr>
           <tr>
-            <th className="text-right py-2"><code>prefix:/user/</code></th>
+            <th className="py-2"><code>prefix:/user/</code></th>
             <td><h6 className="m-0">{ t('search_help.prefix.desc', { path: '/user/' }) }</h6></td>
           </tr>
           <tr>
-            <th className="text-right py-2"><code>-prefix:/user/</code></th>
+            <th className="py-2"><code>-prefix:/user/</code></th>
             <td><h6 className="m-0">{ t('search_help.exclude_prefix.desc', { path: '/user/' }) }</h6></td>
           </tr>
           <tr>
-            <th className="text-right py-2"><code>tag:wiki</code></th>
+            <th className="py-2"><code>tag:wiki</code></th>
             <td><h6 className="m-0">{ t('search_help.tag.desc', { tag: 'wiki' }) }</h6></td>
           </tr>
           <tr>
-            <th className="text-right py-2"><code>-tag:wiki</code></th>
+            <th className="py-2"><code>-tag:wiki</code></th>
             <td><h6 className="m-0">{ t('search_help.exclude_tag.desc', { tag: 'wiki' }) }</h6></td>
           </tr>
         </tbody>

+ 3 - 3
src/client/js/components/Sidebar/SidebarNav.jsx

@@ -53,7 +53,7 @@ class SidebarNav extends React.Component {
   }
 
   render() {
-    const { isAdmin, currentUsername } = this.props.appContainer;
+    const { isAdmin, currentUsername, isSharedUser } = this.props.appContainer;
     const isLoggedIn = currentUsername != null;
 
     const { PrimaryItem, SecondaryItem } = this;
@@ -61,8 +61,8 @@ class SidebarNav extends React.Component {
     return (
       <div className="grw-sidebar-nav d-flex flex-column justify-content-between pb-4">
         <div className="grw-sidebar-nav-primary-container">
-          <PrimaryItem id="custom" label="Custom Sidebar" iconName="code" />
-          <PrimaryItem id="recent" label="Recent Changes" iconName="update" />
+          {!isSharedUser && <PrimaryItem id="custom" label="Custom Sidebar" iconName="code" />}
+          {!isSharedUser && <PrimaryItem id="recent" label="Recent Changes" iconName="update" />}
           {/* <PrimaryItem id="tag" label="Tags" iconName="icon-tag" /> */}
           {/* <PrimaryItem id="favorite" label="Favorite" iconName="icon-star" /> */}
         </div>

+ 3 - 0
src/client/js/services/AppContainer.js

@@ -57,6 +57,9 @@ export default class AppContainer extends Container {
       this.currentUser = JSON.parse(currentUserElem.textContent);
     }
 
+    const isSharedPageElem = document.getElementById('is-shared-page');
+    this.isSharedUser = (isSharedPageElem != null);
+
     const userAgent = window.navigator.userAgent.toLowerCase();
     this.isMobile = /iphone|ipad|android/.test(userAgent);
 

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

@@ -15,7 +15,6 @@ body {
 }
 
 #page-wrapper {
-  min-width: 0;
   margin-top: $grw-navbar-height + $grw-navbar-border-width;
 }
 

+ 3 - 0
src/client/styles/scss/_navbar.scss

@@ -1,5 +1,8 @@
 .grw-navbar {
+  border-top: 0;
+  border-right: 0;
   border-bottom: $grw-navbar-border-width solid;
+  border-left: 0;
 
   .grw-navbar-toggler {
     padding: 0.5rem;

+ 4 - 10
src/client/styles/scss/_on-edit.scss

@@ -26,8 +26,10 @@ body.on-edit {
   }
 
   // show
-  .d-edit-block {
-    display: block !important;
+  .d-edit-sm-block {
+    @include media-breakpoint-up(sm) {
+      display: block !important;
+    }
   }
 
   // hide unnecessary elements
@@ -134,14 +136,6 @@ body.on-edit {
     }
   }
 
-  // deal with word wrap problem
-  // see: https://qiita.com/mpyw/items/dfc63c1fed5dfc5eda26
-  .page-editor-editor-container,
-  .page-editor-preview-container {
-    min-width: 0;
-    overflow-wrap: break-word;
-  }
-
   /*********************
    * Navigation styles
    */

+ 5 - 0
src/client/styles/scss/_override-bootstrap.scss

@@ -130,4 +130,9 @@
     margin-bottom: 18px;
     overflow: hidden;
   }
+
+  .text-break {
+    word-break: break-word;
+    overflow-wrap: break-word;
+  }
 }

+ 57 - 3
src/client/styles/scss/_search.scss

@@ -66,13 +66,22 @@
     border-bottom-left-radius: 40px;
   }
 
+  .search-typeahead {
+    // corner radius
+    border-top-right-radius: 40px;
+    border-bottom-right-radius: 40px;
+    .rbt-input-main {
+      padding-right: 58px;
+      // corner radius
+      border-top-right-radius: 40px;
+      border-bottom-right-radius: 40px;
+    }
+  }
+
   // using react-bootstrap-typeahead
   // see: https://github.com/ericgio/react-bootstrap-typeahead
   .rbt-input.form-control {
     height: 30px;
-    border-top-right-radius: 40px;
-    border-bottom-right-radius: 40px;
-
     .rbt-input-wrapper {
       margin-left: 8px;
     }
@@ -120,6 +129,22 @@
       @include media-breakpoint-up(md) {
         width: 300px;
       }
+      @include media-breakpoint-up(lg) {
+        // focus
+        &.focus {
+          width: 400px;
+        }
+      }
+      @include media-breakpoint-up(xl) {
+        width: 350px;
+        // focus
+        &.focus {
+          width: 450px;
+        }
+      }
+    }
+    .search-typeahead {
+      border-radius: 0 25px 25px 0;
     }
   }
 }
@@ -210,3 +235,32 @@
     display: table-header-group;
   }
 }
+
+@include media-breakpoint-down(sm) {
+  .grw-search-table {
+    th {
+      text-align: right;
+    }
+  
+    td {
+      overflow-wrap: anywhere;
+      white-space: normal !important;
+    }
+  
+    @include media-breakpoint-down(xs) {
+      th,
+      td {
+        display: block;
+      }
+      
+      th {
+        text-align: left;
+      }
+      
+      td {
+        border-top: none !important;
+        padding-top: 0 !important;
+      }
+    }
+  }
+}

+ 3 - 0
src/client/styles/scss/atoms/_nav.scss

@@ -1,6 +1,9 @@
 .nav-tabs .grw-main-nav-item-left {
   width: $grw-nav-main-left-tab-width;
   text-align: center;
+  @include media-breakpoint-down(xs) {
+    width: 45px;
+  }
 
   .nav-link {
     padding-right: 0;

+ 8 - 0
src/client/styles/scss/style-app.scss

@@ -76,6 +76,14 @@
  * Helper Classes
  */
 
+.mw-0 {
+  min-width: 0;
+}
+
+.flex-basis-0 {
+  flex-basis: 0;
+}
+
 .picture {
   width: 24px;
   height: 24px;

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

@@ -38,18 +38,6 @@ $table-hover-bg: $bgcolor-table-hover;
   background-color: darken($bgcolor-global, 5%);
 }
 
-/*
- * GROWI search-top
- */
-.search-top {
-  .btn-group-dropdown-scope .dropdown-toggle {
-    background-color: rgba($bgcolor-global, 0.8);
-  }
-  .rbt-input.form-control {
-    background-color: rgba($bgcolor-global, 0.9);
-  }
-}
-
 /*
  * GROWI subnavigation
  */

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

@@ -154,6 +154,11 @@ pre:not(.hljs):not(.CodeMirror-line) {
   .btn-secondary.dropdown-toggle {
     @include button-variant($bgcolor-search-top-dropdown, $bgcolor-search-top-dropdown);
   }
+
+  // for https://youtrack.weseek.co.jp/issue/GW-2603
+  .search-typeahead {
+    background-color: rgba($bgcolor-global, 0.9);
+  }
 }
 
 .grw-sidebar {

+ 1 - 1
src/lib/components/PagePathHierarchicalLink.jsx

@@ -46,7 +46,7 @@ const PagePathHierarchicalLink = (props) => {
   const RootElm = ({ children }) => {
     return props.isInnerElem
       ? <>{children}</>
-      : <span className="grw-page-path-hierarchical-link">{children}</span>;
+      : <span className="grw-page-path-hierarchical-link text-break">{children}</span>;
   };
 
   return (

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

@@ -15,4 +15,5 @@ module.exports = {
   GlobalNotificationSetting: require('./GlobalNotificationSetting'),
   GlobalNotificationMailSetting: require('./GlobalNotificationSetting/GlobalNotificationMailSetting'),
   GlobalNotificationSlackSetting: require('./GlobalNotificationSetting/GlobalNotificationSlackSetting'),
+  ShareLink: require('./share-link'),
 };

+ 29 - 0
src/server/models/share-link.js

@@ -0,0 +1,29 @@
+// disable no-return-await for model functions
+/* eslint-disable no-return-await */
+
+const mongoose = require('mongoose');
+const uniqueValidator = require('mongoose-unique-validator');
+
+const ObjectId = mongoose.Schema.Types.ObjectId;
+
+/*
+ * define schema
+ */
+const schema = new mongoose.Schema({
+  relatedPage: {
+    type: ObjectId,
+    ref: 'Page',
+    required: true,
+    index: true,
+  },
+  expiration: { type: Date },
+  description: { type: String },
+  createdAt: { type: Date, default: Date.now, required: true },
+});
+
+schema.plugin(uniqueValidator);
+
+module.exports = function(crowi) {
+  const model = mongoose.model('ShareLink', schema);
+  return model;
+};

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

@@ -39,6 +39,7 @@ module.exports = (crowi) => {
 
   router.use('/page', require('./page')(crowi));
   router.use('/pages', require('./pages')(crowi));
+  router.use('/share-links', require('./share-links')(crowi));
 
   router.use('/bookmarks', require('./bookmarks')(crowi));
 

+ 81 - 0
src/server/routes/apiv3/share-links.js

@@ -0,0 +1,81 @@
+// TODO remove this setting after implemented all
+/* eslint-disable no-unused-vars */
+const loggerFactory = require('@alias/logger');
+
+const logger = loggerFactory('growi:routes:apiv3:share-links');
+
+const express = require('express');
+
+const router = express.Router();
+
+const { body } = require('express-validator/check');
+
+const ErrorV3 = require('../../models/vo/error-apiv3');
+
+/**
+ * @swagger
+ *  tags:
+ *    name: ShareLinks
+ */
+
+module.exports = (crowi) => {
+  const loginRequired = require('../../middleware/login-required')(crowi);
+  const csrf = require('../../middleware/csrf')(crowi);
+
+  const ShareLink = crowi.model('ShareLink');
+
+  // TDOO write swagger
+  router.get('/', loginRequired, async(req, res) => {
+    const { pageId } = req.query;
+    // TODO GW-2616 get all share links associated with the page
+  });
+
+
+  // TDOO write swagger
+  router.post('/', loginRequired, async(req, res) => {
+    const { pageId } = req.body;
+    // TODO GW-2609 publish the share link
+  });
+
+  // TDOO write swagger
+  router.delete('/all', loginRequired, async(req, res) => {
+    const { pageId } = req.body;
+    // TODO GW-2694 Delete all share links
+  });
+
+  /**
+  * @swagger
+  *
+  *    /share-links/{id}:
+  *      delete:
+  *        tags: [ShareLinks]
+  *        description: delete one share link related one page
+  *        parameters:
+  *          - name: id
+  *            in: path
+  *            required: true
+  *            description: id of share link
+  *            schema:
+  *              type: string
+  *        responses:
+  *          200:
+  *            description: Succeeded to delete one share link
+  */
+  router.delete('/:id', loginRequired, csrf, async(req, res) => {
+    const { id } = req.params;
+
+    try {
+      const deletedShareLink = await ShareLink.findOneAndRemove({ _id: id });
+      return res.apiv3(deletedShareLink);
+    }
+    catch (err) {
+      const msg = 'Error occurred in delete share link';
+      logger.error('Error', err);
+      return res.apiv3Err(new ErrorV3(msg, 'delete-shareLink-failed'));
+    }
+
+  });
+
+
+  return router;
+};

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

@@ -180,6 +180,8 @@ module.exports = function(crowi, app) {
   app.post('/_api/hackmd.discard'        , accessTokenParser , loginRequiredStrictly , csrf, hackmd.validateForApi, hackmd.discard);
   app.post('/_api/hackmd.saveOnHackmd'   , accessTokenParser , loginRequiredStrictly , csrf, hackmd.validateForApi, hackmd.saveOnHackmd);
 
+  app.get('/share/:linkId', page.showSharedPage, page.notFound);
+
   app.get('/*/$'                   , loginRequired , page.showPageWithEndOfSlash, page.notFound);
   app.get('/*'                     , loginRequired , page.showPage, page.notFound);
 

+ 25 - 0
src/server/routes/page.js

@@ -142,6 +142,7 @@ module.exports = function(crowi, app) {
   const PageTagRelation = crowi.model('PageTagRelation');
   const UpdatePost = crowi.model('UpdatePost');
   const GlobalNotificationSetting = crowi.model('GlobalNotificationSetting');
+  const ShareLink = crowi.model('ShareLink');
 
   const ApiResponse = require('../util/apiResponse');
   const getToday = require('../util/getToday');
@@ -439,6 +440,30 @@ module.exports = function(crowi, app) {
     return showPageForGrowiBehavior(req, res, next);
   };
 
+  actions.showSharedPage = async function(req, res, next) {
+    const { linkId } = req.params;
+
+    const layoutName = configManager.getConfig('crowi', 'customize:layout');
+    const view = `layout-${layoutName}/shared_page`;
+
+    const shareLink = await ShareLink.find({ _id: linkId }).populate('Page');
+    const page = shareLink.relatedPage;
+
+    if (page == null) {
+      // page is not found
+      // TODO GW-2735 create not found page
+      // return res.render(`layout-${layoutName}/not_found_shared_page`);
+    }
+
+    const renderVars = {};
+
+    addRendarVarsForPage(renderVars, page);
+    addRendarVarsForScope(renderVars, page);
+
+    await interceptorManager.process('beforeRenderPage', req, res, renderVars);
+    return res.render(view, renderVars);
+  };
+
   /**
    * switch action by behaviorType
    */

+ 8 - 12
src/server/views/layout-growi/base/layout.html

@@ -15,20 +15,16 @@
 </header>
 {% endblock %}
 
-<div class="container-fluid">
-  <div class="row">
-    <div id="main" class="main col-md-12 {% if page %}{{ css.grant(page) }}{% endif %} {% block main_css_class %}{% endblock %}">
-      {% block content_main_before %}
-      {% endblock %}
+<div id="main" class="main container-fluid {% if page %}{{ css.grant(page) }}{% endif %} {% block main_css_class %}{% endblock %}">
+  {% block content_main_before %}
+  {% endblock %}
 
-      {% block content_main %}
-      {% endblock content_main %}
+  {% block content_main %}
+  {% endblock content_main %}
 
-      {% block content_main_after %}
-      {% endblock %}
-    </div><!-- /.main -->
-  </div><!-- /.row -->
-</div><!-- /.container-fluid -->
+  {% block content_main_after %}
+  {% endblock %}
+</div><!-- /.main -->
 
 <footer class="footer">
   {% include '../../widget/system-version.html' %}

+ 44 - 0
src/server/views/layout-growi/shared_page.html

@@ -0,0 +1,44 @@
+{% extends 'base/layout.html' %}
+
+
+{% block content_header %}
+  <h1 class="p-3">{{ encodeURI(page.path) }}</h1>
+{% endblock %}
+
+
+{% block content_main_before %}
+{% endblock %}
+{% block search %}
+{% endblock %}
+{% block head_warn_alert_siteurl_undefined %}
+{% endblock %}
+
+{% block content_main %}
+  <div class="row" id="is-shared-page">
+    <div class="col grw-page-content-container">
+
+      {% include '../widget/page_content.html' %}
+      {# force remove #revision-toc from #content_main of parent #}
+      <script>
+        $('#revision-toc').remove();
+      </script>
+
+    </div>
+
+    {# relocate #revision-toc #}
+    <div class="col-xl-2 col-lg-3 d-none d-lg-block revision-toc-container">
+      <div id="revision-toc" class="revision-toc mt-3 sps sps--abv" data-sps-offset="123">
+        <div id="revision-toc-content" class="revision-toc-content"></div>
+      </div>
+    </div>
+
+  </div>
+
+{% endblock %}
+
+
+{% block body_end %}
+  <div id="presentation-layer" class="fullscreen-layer">
+    <div id="presentation-container"></div>
+  </div>
+{% endblock %}

+ 6 - 8
src/server/views/layout-growi/widget/liker-and-seenusers.html

@@ -1,16 +1,14 @@
 <div class="liker-and-seenusers">
-  <div class="text-right">
-    {% if page.liker.length > 10 %}<span class="text-muted">..</span>{% endif %}
-    <span id="liker-list" class="mr-3" data-user-ids="{{ page.liker|slice(-9)|default([])|join(',') }}"></span>
+  <div class="text-truncate text-muted text-right" style="direction: rtl;">
     <span class="text-info">
-      <i class="icon-fw icon-like"></i><span class="liker-user-count">{{ page.liker.length|default(0) }}</span>
+      <span class="liker-user-count">{{ page.liker.length|default(0) }}</span><i class="icon-fw icon-like"></i>
     </span>
+    <span id="liker-list" class="mr-1" data-user-ids="{{ page.liker|slice(-15)|default([])|reverse|join(',') }}"></span>
   </div>
-  <div class="text-right">
-    {% if page.seenUsers.length > 10 %}<span class="text-muted">..</span>{% endif %}
-    <span id="seen-user-list" class="mr-3" data-user-ids="{{ page.seenUsers|slice(-9)|default([])|join(',') }}"></span>
+  <div class="text-truncate text-muted text-right" style="direction: rtl;">
     <span class="text-danger">
-      <i class="icon-fw fa fa-paw"></i><span class="seen-user-count">{{ page.seenUsers.length|default(0) }}</span>
+      <span class="seen-user-count">{{ page.seenUsers.length|default(0) }}</span><i class="fa fa-fw fa-paw"></i>
     </span>
+    <span id="seen-user-list" class="mr-1" data-user-ids="{{ page.seenUsers|slice(-15)|default([])|reverse|join(',') }}"></span>
   </div>
 </div>

+ 43 - 0
src/server/views/layout-kibela/shared_page.html

@@ -0,0 +1,43 @@
+{% extends 'base/layout.html' %}
+
+
+{% block content_header %}
+  <h1 class="p-3">{{ encodeURI(page.path) }}</h1>
+{% endblock %}
+
+
+{% block content_main_before %}
+{% endblock %}
+{% block search %}
+{% endblock %}
+{% block head_warn_alert_siteurl_undefined %}
+{% endblock %}
+
+{% block content_main %}
+  <div class="row" id="is-shared-page">
+    <div class="col-12 col-xl-9 col-lg-8 bg-white round-corner">
+
+      {% include '../widget/page_content.html' %}
+      {# force remove #revision-toc from #content_main of parent #}
+      <script>
+        $('#revision-toc').remove();
+      </script>
+
+    </div>
+
+    {# relocate #revision-toc #}
+    <div class="col-xl-3 col-lg-4 d-none d-lg-block revision-toc-container">
+      <div id="revision-toc" class="revision-toc mt-3 sps sps--abv" data-sps-offset="123">
+        <div id="revision-toc-content" class="revision-toc-content"></div>
+      </div>
+    </div>
+
+  </div>
+{% endblock %}
+
+
+{% block body_end %}
+  <div id="presentation-layer" class="fullscreen-layer">
+    <div id="presentation-container"></div>
+  </div>
+{% endblock %}

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

@@ -118,13 +118,14 @@
     {# Sidebar #}
     <div id="grw-sidebar-wrapper"></div>
 
-    <div class="flex-grow-1">
+    <div class="flex-fill mw-0">
       {% block head_warn_alert_siteurl_undefined %}{% include '../widget/alert_siteurl_undefined.html' %}{% endblock %}
 
-      {# Search #}
-      {% if isSearchServiceConfigured() %}
-        <div id="grw-search-top" class="search-top" role="search"></div>
-      {% endif %}
+      {% block search %}
+        {% if isSearchServiceConfigured() %}
+          <div id="grw-search-top" class="search-top" role="search"></div>
+        {% endif %}
+      {% endblock %}
 
       {% block layout_main %}{% endblock %}
     </div>

+ 3 - 3
src/server/views/login/error.html

@@ -2,7 +2,7 @@
 
 {% block html_base_css %}error nologin{% endblock %}
 
-{% block html_title %}{{ customizeService.generateCustomTitleForFixedPageName('セットアップ') }}{% endblock %}
+{% block html_title %}{{ customizeService.generateCustomTitleForFixedPageName(t('login.Setup')) }}{% endblock %}
 
 
 
@@ -33,11 +33,11 @@
       <div class="mb-4 login-form-errors text-center">
         {% if reason === 'registered'%}
         <div class="alert alert-success">
-          <h2>登録完了</h2>
+          <h2>{{ t('login.Registration successful') }}</h2>
         </div>
         {% else %}
         <div class="alert alert-warning">
-            <h2>ログインエラー</h2>
+            <h2>{{ t('login.Sign in error') }}</h2>
         </div>
         {% endif %}
       </div>

+ 1 - 1
src/server/views/widget/not_found_tabs.html

@@ -15,6 +15,6 @@
     </a>
   </li>
 
-  <div id="page-editor-path-nav" class="d-none d-edit-block ml-2"></div>
+  <div id="page-editor-path-nav" class="d-none d-edit-sm-block ml-2"></div>
   {% endif %}
 </ul>

+ 6 - 6
src/server/views/widget/page_tabs.html

@@ -6,7 +6,7 @@
   #}
   <li class="nav-item grw-main-nav-item-left">
     <a class="nav-link active" href="#revision-body" role="tab" data-toggle="tab">
-      <i class="icon-control-play"></i> View
+      <i class="icon-control-play icon-fw"></i><span class="d-none d-sm-inline">View</span>
     </a>
   </li>
 
@@ -19,7 +19,7 @@
           data-toggle="tooltip" data-placement="top" data-container="body" title="{{ t('Not available for guest') }}"
         {% endif %}
       >
-        <i class="icon-note"></i> {{ t('Edit') }}
+        <i class="icon-note icon-fw"></i><span class="d-none d-sm-inline">{{ t('Edit') }}</span>
       </a>
     </li>
 
@@ -32,12 +32,12 @@
           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') }}
+        <i class="fa fa-fw fa-file-text-o"></i><span class="d-none d-sm-inline">{{ t('HackMD') }}</span>
       </a>
     </li>
     {% endif %}
 
-    <div id="page-editor-path-nav" class="d-none d-edit-block ml-2"></div>
+    <div id="page-editor-path-nav" class="d-none d-edit-sm-block ml-2"></div>
   {% endif %}
 
   {#
@@ -51,7 +51,7 @@
   {% if not page.isTopPage() %}
     <li class="nav-item">
       <a href="?presentation=1" class="nav-link toggle-presentation">
-        <i class="icon-film"></i><span class="d-none d-md-inline"> {{ t('Presentation Mode') }}</span>
+        <i class="icon-film icon-fw"></i><span class="d-none d-md-inline">{{ t('Presentation Mode') }}</span>
       </a>
     </li>
   {% endif %}
@@ -59,7 +59,7 @@
   <!-- revision-history -->
   <li class="nav-item">
     <a class="nav-link" href="#revision-history" role="tab" data-toggle="tab">
-      <i class="icon-layers"></i><span class="d-none d-md-inline"> {{ t('History') }}</span>
+      <i class="icon-layers icon-fw"></i><span class="d-none d-md-inline">{{ t('History') }}</span>
     </a>
   </li>
 

+ 1 - 1
src/server/views/widget/page_tabs_kibela.html

@@ -36,7 +36,7 @@
     </li>
     {% endif %}
 
-    <div id="page-editor-path-nav" class="d-none d-edit-block ml-2"></div>
+    <div id="page-editor-path-nav" class="d-none d-edit-sm-block ml-2"></div>
 
   {% endif %}
 

+ 12 - 4
src/server/views/widget/user_page_content.html

@@ -2,20 +2,28 @@
   <ul class="nav nav-tabs user-page-content-menu mb-4" role="tablist">
     <li class="nav-item">
       <a class="nav-link active" href="#user-bookmark-list" role="tab" data-toggle="tab">
-        <i class="icon-star"></i> Bookmarks
+        <i class="icon-star"></i>
+        <span class="d-none d-sm-inline">Bookmarks</span>
       </a>
     </li>
     <li class="nav-item">
       <a class="nav-link" href="#user-created-list" role="tab" data-toggle="tab">
-        <i class="icon-clock"></i> Recently Created
+        <i class="icon-clock"></i>
+        <span class="d-none d-sm-inline">Recently Created</span>
       </a>
     </li>
     <li class="nav-item">
-      <a class="nav-link" href="#user-draft-list" role="tab" data-toggle="tab"><i class="icon-docs"></i> My Drafts</a>
+      <a class="nav-link" href="#user-draft-list" role="tab" data-toggle="tab">
+        <i class="icon-docs"></i>
+        <span class="d-none d-sm-inline">My Drafts</span>
+      </a>
     </li>
     {% if user._id.toString() == pageUser._id.toString() %}
     <li class="nav-item">
-      <a class="nav-link" href="/me" role="tab"><i class="icon-wrench"></i> Settings</a>
+      <a class="nav-link" href="/me" role="tab">
+        <i class="icon-wrench"></i>
+        <span class="d-none d-sm-inline">Settings</span>
+      </a>
     </li>
     {% endif %}
   </ul>