Procházet zdrojové kódy

Merge pull request #325 from weseek/rc/3.0.0

Rc/3.0.0
Yuki Takei před 8 roky
rodič
revize
e6c35bb3b5
100 změnil soubory, kde provedl 3604 přidání a 1728 odebrání
  1. 2 1
      .eslintrc.js
  2. 4 4
      .github/ISSUE_TEMPLATE.md
  3. 3 2
      .gitignore
  4. 10 2
      CHANGES.md
  5. 47 66
      README.md
  6. 14 4
      THIRD-PARTY-NOTICES.md
  7. 5 5
      app.js
  8. 4 4
      app.json
  9. 2 2
      bin/wercker/trigger-growi-docker.sh
  10. 5 5
      config/env.dev.js
  11. 15 9
      config/webpack.common.js
  12. 3 0
      config/webpack.dev.js
  13. 3 0
      config/webpack.prod.js
  14. 19 52
      lib/crowi/dev.js
  15. 19 6
      lib/crowi/index.js
  16. 9 0
      lib/form/admin/customtheme.js
  17. 9 0
      lib/form/admin/customtitle.js
  18. 8 0
      lib/form/admin/userGroupCreate.js
  19. 3 0
      lib/form/index.js
  20. 7 4
      lib/locales/en-US/translation.json
  21. 6 5
      lib/locales/ja/translation.json
  22. 26 8
      lib/models/config.js
  23. 3 0
      lib/models/index.js
  24. 280 0
      lib/models/page-group-relation.js
  25. 75 11
      lib/models/page.js
  26. 1 1
      lib/models/revision.js
  27. 279 0
      lib/models/user-group-relation.js
  28. 151 0
      lib/models/user-group.js
  29. 3 2
      lib/models/user.js
  30. 8 7
      lib/plugins/plugin-utils.js
  31. 315 2
      lib/routes/admin.js
  32. 15 0
      lib/routes/index.js
  33. 9 5
      lib/routes/installer.js
  34. 3 2
      lib/routes/login.js
  35. 44 25
      lib/routes/page.js
  36. 2 1
      lib/util/mailer.js
  37. 2 2
      lib/util/middlewares.js
  38. 1 1
      lib/util/search.js
  39. 1 1
      lib/util/slack.js
  40. 42 3
      lib/util/swigFunctions.js
  41. 20 18
      lib/views/_form.html
  42. 8 8
      lib/views/admin/app.html
  43. 130 23
      lib/views/admin/customize.html
  44. 16 14
      lib/views/admin/external-accounts.html
  45. 9 5
      lib/views/admin/index.html
  46. 9 9
      lib/views/admin/markdown.html
  47. 22 22
      lib/views/admin/notification.html
  48. 2 2
      lib/views/admin/search.html
  49. 18 18
      lib/views/admin/security.html
  50. 248 0
      lib/views/admin/user-group-detail.html
  51. 173 0
      lib/views/admin/user-groups.html
  52. 85 63
      lib/views/admin/users.html
  53. 9 8
      lib/views/admin/widget/menu.html
  54. 5 5
      lib/views/admin/widget/passport/google-oauth.html
  55. 7 7
      lib/views/admin/widget/passport/ldap.html
  56. 7 0
      lib/views/admin/widget/theme-colorbox.html
  57. 0 48
      lib/views/crowi-plus/base/not_found_nosidebar.html
  58. 0 48
      lib/views/crowi-plus/base/page_list_nosidebar.html
  59. 0 48
      lib/views/crowi-plus/base/page_nosidebar.html
  60. 0 72
      lib/views/crowi-plus/base/user_page_nosidebar.html
  61. 0 34
      lib/views/crowi-plus/not_found.html
  62. 0 57
      lib/views/crowi-plus/page.html
  63. 0 48
      lib/views/crowi-plus/page_list.html
  64. 0 24
      lib/views/crowi-plus/user_page.html
  65. 0 69
      lib/views/crowi-plus/widget/header.html
  66. 3 3
      lib/views/customlayout-selector/not_found.html
  67. 3 3
      lib/views/customlayout-selector/page.html
  68. 3 3
      lib/views/customlayout-selector/page_list.html
  69. 3 3
      lib/views/customlayout-selector/user_page.html
  70. 0 15
      lib/views/index.html
  71. 82 58
      lib/views/installer.html
  72. 90 74
      lib/views/invited.html
  73. 51 0
      lib/views/layout-crowi/base/layout.html
  74. 41 0
      lib/views/layout-crowi/not_found.html
  75. 68 0
      lib/views/layout-crowi/page.html
  76. 98 0
      lib/views/layout-crowi/page_list.html
  77. 19 0
      lib/views/layout-crowi/user_page.html
  78. 5 5
      lib/views/layout-crowi/widget/page_side_content.html
  79. 6 6
      lib/views/layout-crowi/widget/page_side_header.html
  80. 33 0
      lib/views/layout-growi/base/layout.html
  81. 20 0
      lib/views/layout-growi/not_found.html
  82. 63 0
      lib/views/layout-growi/page.html
  83. 60 0
      lib/views/layout-growi/page_list.html
  84. 67 0
      lib/views/layout-growi/user_page.html
  85. 5 5
      lib/views/layout-growi/widget/comments.html
  86. 51 0
      lib/views/layout-growi/widget/header.html
  87. 0 57
      lib/views/layout/2column.html
  88. 5 1
      lib/views/layout/admin.html
  89. 128 112
      lib/views/layout/layout.html
  90. 0 22
      lib/views/layout/single-nologin.html
  91. 0 45
      lib/views/layout/single.html
  92. 250 205
      lib/views/login.html
  93. 35 38
      lib/views/login/error.html
  94. 12 10
      lib/views/me/api_token.html
  95. 21 21
      lib/views/me/external-accounts.html
  96. 63 75
      lib/views/me/index.html
  97. 12 16
      lib/views/me/password.html
  98. 29 31
      lib/views/modal/create_page.html
  99. 36 20
      lib/views/modal/delete.html
  100. 17 8
      lib/views/modal/duplicate.html

+ 2 - 1
.eslintrc.js

@@ -31,7 +31,8 @@ module.exports = {
     ],
     "indent": [
       "error",
-      2
+      2,
+      { "SwitchCase": 1 }
     ],
     "key-spacing": [
       "error", { "beforeColon": false, "afterColon": true }

+ 4 - 4
.github/ISSUE_TEMPLATE.md

@@ -6,15 +6,15 @@ Environment
 | item     | version |
 | ---      | --- |
 |OS        ||
-|crowi-plus|x.y.z|
+|GROWI     |x.y.z|
 |node.js   |x.y.z|
 |npm       |x.y.z|
 |Using Docker|yes/no|
-|Using [crowi-plus-docker-compose][crowi-plus-docker-compose]|yes/no|
+|Using [growi-docker-compose][growi-docker-compose]|yes/no|
 
-[crowi-plus-docker-compose]: https://github.com/weseek/crowi-plus-docker-compose
+[growi-docker-compose]: https://github.com/weseek/growi-docker-compose
 
-*(Accessing https://{CROWI_HOST}/admin helps you to fill in above versions)*
+*(Accessing https://{GROWI_HOST}/admin helps you to fill in above versions)*
 
 
 ### Client

+ 3 - 2
.gitignore

@@ -7,7 +7,6 @@ npm-debug.log.*
 .DS_Store
 .Trash-*
 ehthumbs.db
-Icon?
 Thumbs.db
 
 # Node Files #
@@ -18,7 +17,9 @@ npm-debug.log
 
 # Dist #
 /report/
-/public/
+/public/dll
+/public/js
+/public/uploads
 /src/*/__build__/
 /__build__/**
 /src/*/dist/

+ 10 - 2
CHANGES.md

@@ -1,9 +1,17 @@
 CHANGES
 ========
 
-## 2.4.5-RC
+## 3.0.0-RC
 
-* 
+* Feature: Group Access Control List
+* Feature: Add theme selector
+* Support: Add dark theme
+* Support: Refreshing bootstrap theme and icons
+* Support: Use Browsersync instead of easy-livereload
+* Support: Upgrade libs
+    * react-bootstrap
+    * react-bootstrap-typeahead
+    * react-clipboard.js
 
 ## 2.4.4
 

+ 47 - 66
README.md

@@ -1,55 +1,38 @@
-![Crowi](http://res.cloudinary.com/hrscywv4p/image/upload/c_limit,f_auto,h_900,q_80,w_1200/v1/199673/https_www_filepicker_io_api_file_VpYEP32ZQyCZ85u6XCXo_zskpra.png)
+<p align="center">
+  <a href="https://growi.org">
+    <img src="https://user-images.githubusercontent.com/1638767/38254268-d4476bbe-3793-11e8-964c-8865d690baff.png" width="240px">
+  </a>
+</p>
 
 <p align="center">
-  <a href="https://heroku.com/deploy"><img src="https://www.herokucdn.com/deploy/button.png"></a>
+  <a href="https://demo.growi.org">Demo Site</a>
 </p>
 <p align="center">
-  <a href="https://demo.crowi-plus.org">Demo Site</a>
+  <a href="https://github.com/weseek/crowi-plus/releases/latest"><img src="https://img.shields.io/github/release/weseek/crowi-plus.svg"></a>
+  <a href="https://heroku.com/deploy"><img src="https://www.herokucdn.com/deploy/button.png"></a>
+  <a href="https://growi-slackin.weseek.co.jp/"><img src="https://crowi-plus-slackin.weseek.co.jp/badge.svg"></a>
 </p>
 
-crowi-plus [![Chat on Slack](https://crowi-plus-slackin.weseek.co.jp/badge.svg)][slackin]
+GROWI 
 ===========
 
 [![wercker status](https://app.wercker.com/status/39cdc49d067d65c39cb35d52ceae6dc1/s/master "wercker status")](https://app.wercker.com/project/byKey/39cdc49d067d65c39cb35d52ceae6dc1)
-[![dependencies status](https://david-dm.org/weseek/crowi-plus.svg)](https://david-dm.org/weseek/crowi-plus)
-[![devDependencies Status](https://david-dm.org/weseek/crowi-plus/dev-status.svg)](https://david-dm.org/weseek/crowi-plus?type=dev)
-[![docker pulls](https://img.shields.io/docker/pulls/weseek/crowi-plus.svg)](https://hub.docker.com/r/weseek/crowi-plus/)
-[![MIT License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE)
-
+[![dependencies status](https://david-dm.org/weseek/growi.svg)](https://david-dm.org/weseek/growi)
+[![devDependencies Status](https://david-dm.org/weseek/growi/dev-status.svg)](https://david-dm.org/weseek/growi?type=dev)
+[![docker pulls](https://img.shields.io/docker/pulls/weseek/growi.svg)](https://hub.docker.com/r/weseek/growi/)
 
-**crowi-plus** is a fork of [Crowi][crowi] which is [perfectly compatible with the official project](https://github.com/weseek/crowi-plus/wiki/Correspondence-table-with-Crowi-version).
-
-
-Why crowi-plus?
-================
 
 * **Pluggable**
-  * You can find plugins from [npm](https://www.npmjs.com/browse/keyword/crowi-plugin) or [github](https://github.com/search?q=topic%3Acrowi-plugin)!
-* **Fast**
-  * Optimize client-side code chunks by Webpack
-  * Optimize the performance when live preview
-  * Adopt faster libs([date-fns](https://github.com/date-fns/date-fns), [pino](https://github.com/pinojs/pino))
-  * Using CDN
-* **Secure**
-  * Prevent XSS (Cross Site Scripting)
-  * Upgrade jQuery to 3.x and other insecure libs
-  * The official Crowi status is [![dependencies Status](https://david-dm.org/crowi/crowi/status.svg)](https://david-dm.org/crowi/crowi) [![devDependencies Status](https://david-dm.org/crowi/crowi/dev-status.svg)](https://david-dm.org/crowi/crowi?type=dev)
-* **Convenient**
+  * You can find plugins from [npm](https://www.npmjs.com/browse/keyword/growi-plugin) or [github](https://github.com/search?q=topic%3Agrowi-plugin)!
+* **Features**
   * Support Authentication with LDAP / Active Directory 
   * Slack Incoming Webhooks Integration
-  * [Miscellaneous features](https://github.com/weseek/crowi-plus/wiki/Additional-Features)
+  * [Miscellaneous features](https://github.com/weseek/growi/wiki/Additional-Features)
 * **[Docker Ready][dockerhub]**
 * **[Docker Compose Ready][docker-compose]**
-  * [Multiple sites example](https://github.com/weseek/crowi-plus-docker-compose/tree/master/examples/multi-app)
-  * [HTTPS(with Let's Encrypt) proxy integration example](https://github.com/weseek/crowi-plus-docker-compose/tree/master/examples/https-portal)
+  * [Multiple sites example](https://github.com/weseek/growi-docker-compose/tree/master/examples/multi-app)
+  * [HTTPS(with Let's Encrypt) proxy integration example](https://github.com/weseek/growi-docker-compose/tree/master/examples/https-portal)
 * Support IE11 (Experimental)
-* **Developer-friendly**
-  * Less compile time
-  * LiveReload separately available by server/client code change
-  * Exclude Environment-dependency (confirmed to be developable on Win/Mac/Linux)
-
-Check out all additional features from [**here**](https://github.com/weseek/crowi-plus/wiki/Additional-Features).
-
 
 Quick Start for Production
 ===========================
@@ -64,26 +47,26 @@ Using docker-compose
 ---------------------
 
 ```bash
-git clone https://github.com/weseek/crowi-plus-docker-compose.git crowi-plus
-cd crowi-plus
+git clone https://github.com/weseek/growi-docker-compose.git growi
+cd growi
 docker-compose up
 ```
 
-See also [weseek/crowi-plus-docker-compose][docker-compose]
+See also [weseek/growi-docker-compose][docker-compose]
 
 On-premise
 ----------
 
-[**Migration Guide from Official Crowi** is here](https://github.com/weseek/crowi-plus/wiki/Migration-Guide-from-Official-Crowi).
+[**Migration Guide from Crowi** is here](https://github.com/weseek/growi/wiki/Migration-Guide-from-Crowi).
 
 ### Dependencies
 
-- node 6.x (DON'T USE 7.x)
-- npm 4.x (DON'T USE 5.x)
+- node 8.x (DON'T USE 9.x)
+- npm 5.x
 - yarn
 - MongoDB 3.x
 
-See [confirmed versions](https://github.com/weseek/crowi-plus/wiki/Developers-Guide#versions-confirmed-to-work).
+See [confirmed versions](https://github.com/weseek/growi/wiki/Developers-Guide#versions-confirmed-to-work).
 
 #### Optional Dependencies
 
@@ -98,24 +81,24 @@ See [confirmed versions](https://github.com/weseek/crowi-plus/wiki/Developers-Gu
 #### Build and run the app
 
 ```bash
-git clone https://github.com/weseek/crowi-plus.git
-cd crowi-plus
+git clone https://github.com/weseek/growi.git
+cd growi
 yarn
-MONGO_URI=mongodb://MONGO_HOST:MONGO_PORT/crowi npm start
+MONGO_URI=mongodb://MONGO_HOST:MONGO_PORT/growi npm start
 ```
 
 **DO NOT USE `npm install`**, use `yarn` instead.
 
-If you launch crowi-plus with Redis and ElasticSearch, add environment variables before `npm start` like following:
+If you launch growi with Redis and ElasticSearch, add environment variables before `npm start` like following:
 
 ```
-export MONGO_URI=mongodb://MONGO_HOST:MONGO_PORT/crowi
-export REDIS_URL=redis://REDIS_HOST:REDIS_PORT/crowi
-export ELASTICSEARCH_URI=http://ELASTICSEARCH_HOST:ELASTICSEARCH_PORT/crowi
+export MONGO_URI=mongodb://MONGO_HOST:MONGO_PORT/growi
+export REDIS_URL=redis://REDIS_HOST:REDIS_PORT/growi
+export ELASTICSEARCH_URI=http://ELASTICSEARCH_HOST:ELASTICSEARCH_PORT/growi
 npm start
 ```
 
-For more info, see [Developers Guide](https://github.com/weseek/crowi-plus/wiki/Developers-Guide) and [the official documents](https://github.com/crowi/crowi/wiki/Install-and-Configuration#env-parameters).
+For more info, see [Developers Guide](https://github.com/weseek/growi/wiki/Developers-Guide) and [Crowi documents](https://github.com/crowi/crowi/wiki/Install-and-Configuration#env-parameters).
 
 #### Command details
 
@@ -137,19 +120,19 @@ npm start
 
 * Stop server if server is running
 * `yarn add` to install plugin or `npm install --save`
-  * **Don't forget `--save` option if you use npm** or crowi-plus doesn't detect plugins
+  * **Don't forget `--save` option if you use npm** or growi doesn't detect plugins
 * `npm start` to build client app and start server
 
 #### Examples
 
 ```bash
-yarn add crowi-plugin-lsx
+yarn add growi-plugin-lsx
 npm start
 ```
 
 
 
-For more info, see [Developers Guide](https://github.com/weseek/crowi-plus/wiki/Developers-Guide) on Wiki.
+For more info, see [Developers Guide](https://github.com/weseek/growi/wiki/Developers-Guide) on Wiki.
 
 
 Environment Variables
@@ -171,10 +154,10 @@ Environment Variables
 Documentation
 ==============
 
-* [github wiki pages](https://github.com/weseek/crowi-plus/wiki)
-  * [Questions and Answers](https://github.com/weseek/crowi-plus/wiki/Questions-and-Answers)
-  * [Migration Guide from Official Crowi](https://github.com/weseek/crowi-plus/wiki/Migration-Guide-from-Official-Crowi)
-  * [Developers Guide](https://github.com/weseek/crowi-plus/wiki/Developers-Guide)
+* [github wiki pages](https://github.com/weseek/growi/wiki)
+  * [Questions and Answers](https://github.com/weseek/growi/wiki/Questions-and-Answers)
+  * [Migration Guide from Crowi](https://github.com/weseek/growi/wiki/Migration-Guide-from-Crowi)
+  * [Developers Guide](https://github.com/weseek/growi/wiki/Developers-Guide)
 
 Contribution
 ============
@@ -185,7 +168,6 @@ For development
 ### Build and Run the app
 
 1. `clone` this repository
-1. `yarn global add npm@4` to install required global dependencies
 1. `yarn` to install all dependencies
     * DO NOT USE `npm install`
 1. `npm run build` to build client app
@@ -196,7 +178,7 @@ Found a Bug?
 -------------
 
 If you found a bug in the source code, you can help us by
-[submitting an issue][issues] to our [GitHub Repository][crowi-plus]. Even better, you can
+[submitting an issue][issues] to our [GitHub Repository][growi]. Even better, you can
 [submit a Pull Request][pulls] with a fix.
 
 Missing a Feature?
@@ -217,7 +199,7 @@ You can write issues and PRs in English or Japanese.
 Discussion
 -----------
 
-If you have questions or suggestions, you can [join our Slack team][slackin] and talk about anything, anytime.
+If you have questions or suggestions, you can [join our Slack team](https://growi-slackin.weseek.co.jp/) and talk about anything, anytime.
 
 
 License
@@ -228,9 +210,8 @@ License
 
 
 [crowi]: https://github.com/crowi/crowi
-[crowi-plus]: https://github.com/weseek/crowi-plus
-[issues]: https://github.com/weseek/crowi-plus/issues
-[pulls]: https://github.com/weseek/crowi-plus/pulls
-[dockerhub]: https://hub.docker.com/r/weseek/crowi-plus
-[docker-compose]: https://github.com/weseek/crowi-plus-docker-compose
-[slackin]: https://crowi-plus-slackin.weseek.co.jp/
+[growi]: https://github.com/weseek/growi
+[issues]: https://github.com/weseek/growi/issues
+[pulls]: https://github.com/weseek/growi/pulls
+[dockerhub]: https://hub.docker.com/r/weseek/growi
+[docker-compose]: https://github.com/weseek/growi-docker-compose

+ 14 - 4
THIRD-PARTY-NOTICES.md

@@ -1,5 +1,5 @@
-crowi-plus uses third-party libraries or other resources that may  
-be distributed under licenses different than the crowi-plus software.
+GROWI uses third-party libraries or other resources that may  
+be distributed under licenses different than the GROWI software.
 
 In the event that we accidentally failed to list a required notice,  
 please bring it to our attention through any of the ways detailed here :
@@ -9,12 +9,12 @@ please bring it to our attention through any of the ways detailed here :
 The attached notices are provided for information only.
 
 For any licenses that require disclosure of source, sources are available at  
-https://github.com/weseek/crowi-plus.
+https://github.com/weseek/growi.
 
 
 1. crowi/crowi (https://github.com/crowi/crowi)
 2. Microsoft/vscode (https://github.com/Microsoft/vscode)
-
+3. stephenhutchings/typicons.font (https://github.com/stephenhutchings/typicons.font)
 
 
 License Notice for Crowi
@@ -78,3 +78,13 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 SOFTWARE.
 
 ```
+
+
+License Notice for Typicons
+------------------------
+
+https://creativecommons.org/licenses/by-sa/3.0/
+
+```
+Copyright (c) 2018 Stephen Hutchings
+```

+ 5 - 5
app.js

@@ -1,11 +1,11 @@
 /**
- * Crowi::app.js
+ * Growi::app.js
  *
- * @package crowi-plus
+ * @package growi
  * @author  Yuki Takei <yuki@weseek.co.jp>
  */
 
-var crowi = new (require('./lib/crowi'))(__dirname, process.env);
+var growi = new (require('./lib/crowi'))(__dirname, process.env);
 
-crowi.start()
-  .catch(crowi.exitOnerror);
+growi.start()
+  .catch(growi.exitOnerror);

+ 4 - 4
app.json

@@ -1,11 +1,11 @@
 {
-  "name": "crowi-plus",
-  "description": "Enhanced Crowi",
+  "name": "growi",
+  "description": "Team collaboration system with markdown",
   "keywords": [
     "wiki",
     "communication"
   ],
-  "repository": "https://github.com/weseek/crowi-plus",
+  "repository": "https://github.com/weseek/growi",
   "success_url": "/",
   "env": {
     "SECRET_TOKEN": {
@@ -18,7 +18,7 @@
     },
     "INSTALL_PLUGINS": {
       "description": "Comma-separated list of plugin package names to install.",
-      "value": "crowi-plugin-lsx",
+      "value": "growi-plugin-lsx",
       "required": false
     }
   },

+ 2 - 2
bin/wercker/trigger-crowi-plus-docker.sh → bin/wercker/trigger-growi-docker.sh

@@ -7,14 +7,14 @@
 #
 # require
 #   - $WERCKER_TOKEN
-#   - $CROWI_PLUS_DOCKER_PIPELINE_ID
+#   - $GROWI_DOCKER_PIPELINE_ID
 #   - $RELEASE_VERSION
 #
 RESPONSE=`curl -X POST \
   -H "Content-Type: application/json" \
   -H "Authorization: Bearer $WERCKER_TOKEN" \
   https://app.wercker.com/api/v3/runs -d '{ \
-    "pipelineId": "'$CROWI_PLUS_DOCKER_PIPELINE_ID'", \
+    "pipelineId": "'$GROWI_DOCKER_PIPELINE_ID'", \
     "branch": "release", \
     "envVars": [ \
       { \

+ 5 - 5
config/env.dev.js

@@ -2,18 +2,18 @@ module.exports = {
   NODE_ENV: 'development',
   FILE_UPLOAD: 'local',
   // MATHJAX: 1,
-  // REDIS_URL: 'redis://localhost:6379/crowi',
-  // ELASTICSEARCH_URI: 'http://localhost:9200/crowi',
+  // REDIS_URL: 'redis://localhost:6379/growi',
+  // ELASTICSEARCH_URI: 'http://localhost:9200/growi',
   PLUGIN_NAMES_TOBE_LOADED: [
-    // 'crowi-plugin-lsx',
-    // 'crowi-plugin-pukiwiki-like-linker',
+    // 'growi-plugin-lsx',
+    // 'growi-plugin-pukiwiki-like-linker',
   ],
   // filters for debug
   DEBUG: [
     // 'express:*',
     // 'crowi:*',
     'crowi:crowi',
-    'crowi:crowi:dev',
+    // 'crowi:crowi:dev',
     'crowi:crowi:express-init',
     'crowi:models:external-account',
     // 'crowi:routes:login',

+ 15 - 9
config/webpack.common.js

@@ -10,7 +10,7 @@ const helpers = require('./helpers');
  */
 const AssetsPlugin = require('assets-webpack-plugin');
 const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');
-const LodashModuleReplacementPlugin = require('lodash-webpack-plugin');;
+const ExtractTextPlugin = require('extract-text-webpack-plugin');
 
 /*
  * Webpack configuration
@@ -27,8 +27,10 @@ module.exports = function (options) {
       'legacy-admin':         './resource/js/legacy/crowi-admin',
       'legacy-presentation':  './resource/js/legacy/crowi-presentation',
       'plugin':               './resource/js/plugin',
-      'style':                './resource/styles',
-      'style-presentation':   './resource/styles/presentation',
+      'style':                './resource/styles/scss/style.scss',
+      'style-theme-default':  './resource/styles/scss/theme/default.scss',
+      'style-theme-default-dark':  './resource/styles/scss/theme/default-dark.scss',
+      'style-presentation':   './resource/styles/scss/style-presentation.scss',
     },
     externals: {
       // require("jquery") is external and available
@@ -53,17 +55,23 @@ module.exports = function (options) {
             }
           }]
         },
+        {
+          test: /\.scss$/,
+          use: ExtractTextPlugin.extract({
+            fallback: 'style-loader',
+            use: 'css-loader!sass-loader'
+          }),
+          include: [helpers.root('resource/styles/scss')]
+        },
         {
           test: /\.css$/,
           use: ['style-loader', 'css-loader'],
-          // comment out 'include' spec for crowi-plugins
-          // include: [helpers.root('resource')]
+          exclude: [helpers.root('resource/styles/scss')]
         },
         {
           test: /\.scss$/,
           use: ['style-loader', 'css-loader', 'sass-loader'],
-          // comment out 'include' spec for crowi-plugins
-          // include: [helpers.root('resource')]
+          exclude: [helpers.root('resource/styles/scss')]
         },
         /*
          * File loader for supporting images, for example, in CSS files.
@@ -102,8 +110,6 @@ module.exports = function (options) {
         chunks: ['commons', 'plugin'],
       }),
 
-      new LodashModuleReplacementPlugin,
-
       // ignore
       new webpack.IgnorePlugin(/^\.\/lib\/deflate\.js/, /markdown-it-plantuml/),
 

+ 3 - 0
config/webpack.dev.js

@@ -13,6 +13,7 @@ const commonConfig = require('./webpack.common.js');
  * Webpack Plugins
  */
 const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');
+const ExtractTextPlugin = require('extract-text-webpack-plugin');
 const DllBundlesPlugin = require('webpack-dll-bundles-plugin').DllBundlesPlugin;
 const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
 
@@ -51,6 +52,8 @@ module.exports = function (options) {
     },
     plugins: [
 
+      new ExtractTextPlugin('[name].bundle.css'),
+
       new DllBundlesPlugin({
         bundles: {
           vendor: [

+ 3 - 0
config/webpack.prod.js

@@ -11,6 +11,7 @@ const commonConfig = require('./webpack.common.js'); // the settings that are co
  * Webpack Plugins
  */
 const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');
+const ExtractTextPlugin = require('extract-text-webpack-plugin');
 const UglifyJsPlugin = require('webpack/lib/optimize/UglifyJsPlugin');
 const OptimizeJsPlugin = require('optimize-js-plugin');
 const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
@@ -45,6 +46,8 @@ module.exports = function (env) {
         }
       }),
 
+      new ExtractTextPlugin('[name].[contenthash].css'),
+
       new OptimizeJsPlugin({
         sourceMap: false
       }),

+ 19 - 52
lib/crowi/dev.js

@@ -6,7 +6,7 @@ const helpers = require('./helpers');
 
 const swig = require('swig-templates');
 const onHeaders = require('on-headers')
-const LRWebSocketServer = require('livereload-server/lib/server');
+
 
 class CrowiDev {
 
@@ -21,11 +21,10 @@ class CrowiDev {
   }
 
   init() {
-    this.requireForLiveReload();
+    this.requireForAutoReloadServer();
 
     this.initPromiseRejectionWarningHandler();
     this.initSwig();
-    this.hackLRWebSocketServer();
   }
 
   initPromiseRejectionWarningHandler() {
@@ -38,44 +37,15 @@ class CrowiDev {
   }
 
   /**
-   * require files for live reloading
+   * require files for node-dev auto reloading
    */
-  requireForLiveReload() {
-    // environment file
-    require(path.join(this.crowi.rootDir, 'config', 'env.dev.js'));
-
+  requireForAutoReloadServer() {
     // load all json files for live reloading
     fs.readdirSync(this.crowi.localeDir).map((dirname) => {
       require(path.join(this.crowi.localeDir, dirname, 'translation.json'));
     });
   }
 
-  /**
-   * prevent to crash socket with:
-   * -------------------------------------------------
-   * Error: read ECONNRESET
-   *     at exports._errnoException (util.js:1022:11)
-   *     at TCP.onread (net.js:569:26)
-   * -------------------------------------------------
-   *
-   * @see https://github.com/napcs/node-livereload/pull/15
-   *
-   * @memberOf CrowiDev
-   */
-  hackLRWebSocketServer() {
-    const orgCreateConnection = LRWebSocketServer.prototype._createConnection;
-
-    // replace https://github.com/livereload/livereload-server/blob/v0.2.3/lib/server.coffee#L74
-    LRWebSocketServer.prototype._createConnection = function(socket) {
-      // call original method with substituting 'this' obj
-      orgCreateConnection.call(this, socket);
-
-      socket.on('error', (err) => {
-        console.warn(`[WARN] An insignificant error occured in client socket: '${err}'`);
-      });
-    }
-  }
-
   /**
    *
    *
@@ -86,7 +56,7 @@ class CrowiDev {
    */
   setup(server, app) {
     this.setupHeaderDebugger(app);
-    this.setupEasyLiveReload(app);
+    this.setupBrowserSync(app);
   }
 
   setupHeaderDebugger(app) {
@@ -100,23 +70,20 @@ class CrowiDev {
     });
   }
 
-  setupEasyLiveReload(app) {
-    if (!helpers.hasProcessFlag('livereload')) {
-      return;
-    }
-
-    debug('setupEasyLiveReload');
-
-    const livereload = require('easy-livereload');
-    app.use(livereload({
-      watchDirs: [
-        path.join(this.crowi.viewsDir),
-        path.join(this.crowi.publicDir),
-      ],
-      checkFunc: function(x) {
-        return /\.(html|css|js)$/.test(x);
-      },
-    }));
+  setupBrowserSync(app) {
+    debug('setupBrowserSync');
+
+    const browserSync = require('browser-sync');
+    const bs = browserSync.create().init({
+      logSnippet: false,
+      notify: false,
+      files: [
+        `${this.crowi.viewsDir}/**/*.html`,
+        `${this.crowi.publicDir}/**/*.js`,
+        `${this.crowi.publicDir}/**/*.css`,
+      ]
+    });
+    app.use(require('connect-browser-sync')(bs));
   }
 
   loadPlugins(app) {

+ 19 - 6
lib/crowi/index.js

@@ -9,6 +9,7 @@ var debug = require('debug')('crowi:crowi')
 
   , mongoose    = require('mongoose')
 
+  , eazyLogger = require('eazy-logger')
   , helpers = require('./helpers')
   , models = require('../models')
   ;
@@ -16,6 +17,13 @@ var debug = require('debug')('crowi:crowi')
 function Crowi (rootdir, env)
 {
   var self = this;
+  // this.logger = easyLogger.Logger({
+  //   prefix: '[{green:GROWI}]'
+  // });
+  this.logger = eazyLogger.Logger({
+    prefix: "[{green:GROWI}] ",
+    useLevelPrefixes: false,
+  });
 
   this.version = pkg.version;
   this.runtimeVersions = undefined;   // initialized by scanRuntimeVersions()
@@ -140,7 +148,7 @@ Crowi.prototype.setupDatabase = function() {
     this.env.MONGODB_URI || // MONGOLAB changes their env name
     this.env.MONGOHQ_URL ||
     this.env.MONGO_URI ||
-    ((process.env.NODE_ENV === 'test') ? 'mongodb://localhost/crowi_test' : 'mongodb://localhost/crowi')
+    ((process.env.NODE_ENV === 'test') ? 'mongodb://localhost/growi_test' : 'mongodb://localhost/growi')
     ;
 
   return mongoose.connect(mongoUri).then(
@@ -368,12 +376,17 @@ Crowi.prototype.start = function() {
     .then(function(app) {
       server = http.createServer(app).listen(self.port, function() {
         debug(`[${self.node_env}] Express server listening on port ${self.port}`);
-      });
 
-      // setup
-      if (self.node_env === 'development') {
-        self.crowiDev.setup(server, app);
-      }
+        self.logger.info('{bold:Server URLs:}');
+        self.logger.unprefixed('info','{grey:=======================================}');
+        self.logger.unprefixed('info',`         APP: {magenta:http://localhost:${self.port}}`);
+        self.logger.unprefixed('info','{grey:=======================================}');
+
+        // setup for dev
+        if (self.node_env === 'development') {
+          self.crowiDev.setup(server, app);
+        }
+      });
 
       io = require('socket.io')(server);
       io.sockets.on('connection', function (socket) {

+ 9 - 0
lib/form/admin/customtheme.js

@@ -0,0 +1,9 @@
+'use strict';
+
+var form = require('express-form')
+  , field = form.field
+  ;
+
+module.exports = form(
+  field('settingForm[customize:theme]')
+);

+ 9 - 0
lib/form/admin/customtitle.js

@@ -0,0 +1,9 @@
+'use strict';
+
+var form = require('express-form')
+  , field = form.field
+  ;
+
+module.exports = form(
+  field('settingForm[customize:title]')
+);

+ 8 - 0
lib/form/admin/userGroupCreate.js

@@ -0,0 +1,8 @@
+'use strict';
+
+var form = require('express-form')
+  , field = form.field;
+
+module.exports = form(
+  field('createGroupForm[userGroupName]', '新規グループ名').trim().required()
+);

+ 3 - 0
lib/form/index.js

@@ -23,6 +23,8 @@ module.exports = {
     customcss: require('./admin/customcss'),
     customscript: require('./admin/customscript'),
     customheader: require('./admin/customheader'),
+    customtheme: require('./admin/customtheme'),
+    customtitle: require('./admin/customtitle'),
     custombehavior: require('./admin/custombehavior'),
     customlayout: require('./admin/customlayout'),
     customfeatures: require('./admin/customfeatures'),
@@ -30,5 +32,6 @@ module.exports = {
     userInvite: require('./admin/userInvite'),
     slackIwhSetting: require('./admin/slackIwhSetting'),
     slackSetting: require('./admin/slackSetting'),
+    userGroupCreate: require('./admin/userGroupCreate'),
   },
 };

+ 7 - 4
lib/locales/en-US/translation.json

@@ -47,6 +47,7 @@
   "View diff": "View diff",
 
   "User ID": "User ID",
+  "Home": "Home",
   "User Settings": "User Settings",
   "User Information": "User Information",
   "Basic Info": "Basic Info",
@@ -75,10 +76,12 @@
 
   "Table of Contents": "Table of Contents",
 
+  "UserGroup management": "UserGroup management",
   "Public": "Public",
   "Anyone with the link": "Anyone with the link",
   "Specified users only": "Specified users only",
   "Just me": "Just me",
+  "Only inside the group": "Only inside the group",
   "Shareable link": "Shareable link",
 
   "Show latest": "Show latest",
@@ -129,7 +132,6 @@
   "New password": "New password",
   "Re-enter new password": "Re-enter new password",
   "Password is not set": "Password is not set",
-  "You can sign in with email and password": "You can sign in with <code>%s</code> and password",
 
   "API Settings": "API Settings",
   "API Token Settings": "API Token Settings",
@@ -143,6 +145,7 @@
       "notice": {
           "version": "This is not the current version.",
           "moved": "This page was moved from <code>%s</code>",
+          "duplicated": "This page was duplicated from <code>%s</code>",
           "unlinked": "Redirect pages to this page have been deleted.",
           "restricted": "Access to this page is restricted"
       }
@@ -250,8 +253,8 @@
     "Plugin settings": "Plugin settings",
     "Enable plugin loading": "Enable plugin loading",
     "Load plugins": "Load plugins",
-    "valid": "Valid",
-    "invalid": "Invalid"
+    "Enable": "Enable",
+    "Disable": "Disable"
 
   },
   "security_setting": {
@@ -265,7 +268,7 @@
     "without_encryption": "Please be noted that your ID and Password will be sent wihtout encryption.",
     "users_without_account": "Users without account is not accessible",
     "restrict_emails": "You can restrict registerable e-mail address.",
-    "for_instance":" For instance, if you use Crowi-plus within a company, you can write ",
+    "for_instance":" For instance, if you use growi within a company, you can write ",
     "only_those":" Only those whose e-mail address including the company address can register.",
     "insert_single":"Please insert single e-mail address per line.",
     "Authentication mechanism settings":"Authentication mechanism settings"

+ 6 - 5
lib/locales/ja/translation.json

@@ -44,6 +44,7 @@
   "View diff": "差分を見る",
 
   "User ID": "ユーザーID",
+  "Home": "ホーム",
   "User Settings": "ユーザー設定",
   "User Information": "ユーザー情報",
   "Basic Info": "ユーザーの基本情報",
@@ -76,14 +77,13 @@
   "Table of Contents": "目次",
   "Management Wiki Home": "Wiki管理トップ",
   "App settings": "アプリ設定",
-  "Security settings": "セキュリティ設定",
   "Markdown settings": "Markdown設定",
   "Customize": "カスタマイズ",
   "Notification settings": "通知設定",
   "User management": "ユーザー管理",
+  "UserGroup management": "グループ管理",
   "Basic settings": "基本設定",
   "Basic authentication": "Basic認証",
-  "Password": "パスワード",
   "Guest users access": "ゲストユーザーのアクセス",
   "Register limitation": "登録の制限",
   "The contents entered here will be shown in the header etc": "ここに入力した内容は、ヘッダー等に表示されます。",
@@ -91,6 +91,7 @@
   "Anyone with the link": "リンクを知っている人のみ",
   "Specified users": "特定ユーザーのみ",
   "Just me": "自分のみ",
+  "Only inside the group": "特定グループのみ",
   "Shareable link": "このページの共有用URL",
   "The whitelist of registration permission E-mail address": "登録許可メールアドレスの<br>ホワイトリスト",
   "Selecting authentication mechanism": "認証機構選択",
@@ -145,7 +146,6 @@
   "New password": "新しいパスワード",
   "Re-enter new password": "(確認用)",
   "Password is not set": "パスワードが設定されていません",
-  "You can sign in with email and password": "<code>%s</code> と設定されたパスワードの組み合わせでログイン可能になります。",
 
   "Security settings": "セキュリティ設定",
 
@@ -158,6 +158,7 @@
       "notice": {
           "version": "これは現在の版ではありません。",
           "moved": "このページは <code>%s</code> から移動しました。",
+          "duplicated": "このページは <code>%s</code> から複製されました。",
           "unlinked": "このページへのリダイレクトは削除されました。",
           "restricted": "このページの閲覧は制限されています"
       }
@@ -267,8 +268,8 @@
     "Plugin settings": "プラグイン設定",
     "Enable plugin loading": "プラグインの読み込みを有効にします。",
     "Load plugins": "プラグインを読み込む",
-    "valid": "有効",
-    "invalid": "無効"
+    "Enable": "有効",
+    "Disable": "無効"
 
    },
 

+ 26 - 8
lib/models/config.js

@@ -21,17 +21,16 @@ module.exports = function(crowi) {
   });
 
   /**
-   * default values when crowi-plus is cleanly installed
+   * default values when GROWI is cleanly installed
    */
   function getArrayForInstalling() {
     let config = getDefaultCrowiConfigs();
 
     // overwrite
-    config['app:title'] = 'crowi-plus';
     config['app:fileUpload'] = true;
     config['security:isEnabledPassport'] = true;
-    config['customize:behavior'] = 'crowi-plus';
-    config['customize:layout'] = 'crowi-plus';
+    config['customize:behavior'] = 'growi';
+    config['customize:layout'] = 'growi';
     config['customize:isSavedStatesOfTabChanges'] = false;
 
     return config;
@@ -44,7 +43,6 @@ module.exports = function(crowi) {
   {
     return {
       //'app:installed'     : "0.0.0",
-      'app:title'         : 'Crowi',
       'app:confidential'  : '',
 
       'app:fileUpload'    : false,
@@ -66,7 +64,7 @@ module.exports = function(crowi) {
       'security:passport-ldap:groupSearchFilter' : undefined,
       'security:passport-ldap:groupDnProperty' : undefined,
 
-      'aws:bucket'          : 'crowi',
+      'aws:bucket'          : 'growi',
       'aws:region'          : 'ap-northeast-1',
       'aws:accessKeyId'     : '',
       'aws:secretAccessKey' : '',
@@ -85,8 +83,10 @@ module.exports = function(crowi) {
       'customize:css' : '',
       'customize:script' : '',
       'customize:header' : '',
+      'customize:title' : '',
       'customize:highlightJsStyle' : 'github',
       'customize:highlightJsStyleBorder' : false,
+      'customize:theme' : 'default',
       'customize:behavior' : 'crowi',
       'customize:layout' : 'crowi',
       'customize:isEnabledTimeline' : true,
@@ -253,9 +253,15 @@ module.exports = function(crowi) {
       });
   };
 
+  configSchema.statics.appTitle = function(config)
+  {
+    const key = 'app:title';
+    return getValueForCrowiNS(config, key) || 'GROWI';
+  };
+
   configSchema.statics.isEnabledPassport = function(config)
   {
-    // always true if crowi-plus installed cleanly
+    // always true if growi installed cleanly
     if (Object.keys(config.crowi).length == 0) {
       return true;
     }
@@ -360,6 +366,18 @@ module.exports = function(crowi) {
     return getValueForCrowiNS(config, key);
   }
 
+  configSchema.statics.theme = function(config)
+  {
+    const key = 'customize:theme';
+    return getValueForCrowiNS(config, key);
+  }
+
+  configSchema.statics.customTitle = function(config)
+  {
+    const key = 'customize:title';
+    return getValueForCrowiNS(config, key);
+  }
+
   configSchema.statics.behaviorType = function(config)
   {
     const key = 'customize:behavior';
@@ -465,7 +483,7 @@ module.exports = function(crowi) {
 
     const local_config = {
       crowi: {
-        title: config.crowi['app:title'],
+        title: Config.appTitle(crowi),
         url: config.crowi['app:url'] || '',
       },
       upload: {

+ 3 - 0
lib/models/index.js

@@ -2,8 +2,11 @@
 
 module.exports = {
   Page: require('./page'),
+  PageGroupRelation: require('./page-group-relation'),
   User: require('./user'),
   ExternalAccount: require('./external-account'),
+  UserGroup: require('./user-group'),
+  UserGroupRelation: require('./user-group-relation'),
   Revision: require('./revision'),
   Bookmark: require('./bookmark'),
   Comment: require('./comment'),

+ 280 - 0
lib/models/page-group-relation.js

@@ -0,0 +1,280 @@
+const debug = require('debug')('crowi:models:pageGroupRelation');
+const mongoose = require('mongoose');
+const mongoosePaginate = require('mongoose-paginate');
+const ObjectId = mongoose.Schema.Types.ObjectId;
+
+
+/*
+ * define schema
+ */
+const schema = new mongoose.Schema({
+  relatedGroup: { type: ObjectId, ref: 'UserGroup', required: true },
+  targetPage: { type: ObjectId, ref: 'Page', required: true },
+  createdAt: { type: Date, default: Date.now },
+}, {
+  toJSON: { getters: true },
+  toObject: { getters: true }
+});
+// apply plugins
+schema.plugin(mongoosePaginate);
+
+
+/**
+ * PageGroupRelation Class
+ *
+ * @class PageGroupRelation
+ */
+class PageGroupRelation {
+
+    /**
+   * limit items num for pagination
+   *
+   * @readonly
+   * @static
+   * @memberof PageGroupRelation
+   */
+   static get PAGE_ITEMS() {
+     return 50;
+    }
+
+  static set crowi(crowi) {
+    this._crowi = crowi;
+  }
+
+  static get crowi() {
+    return this._crowi;
+  }
+
+  /**
+   * find all page and group relation
+   *
+   * @static
+   * @returns {Promise<PageGroupRelation[]>}
+   * @memberof PageGroupRelation
+   */
+  static findAllRelation() {
+
+    return this
+      .find()
+      .populate('targetPage')
+      .exec();
+  }
+
+  /**
+   * find all page and group relation for UserGroup
+   *
+   * @static
+   * @param {UserGroup} userGroup
+   * @returns {Promise<PageGroupRelation[]>}
+   * @memberof PageGroupRelation
+   */
+  static findAllRelationForUserGroup(userGroup) {
+    debug('findAllRelationForUserGroup is called', userGroup);
+
+    return this
+      .find({ relatedGroup: userGroup.id })
+      .populate('targetPage')
+      .exec();
+  }
+
+  /**
+   * find all entities with pagination
+   *
+   * @see https://github.com/edwardhotchkiss/mongoose-paginate
+   *
+   * @static
+   * @param {UserGroup} userGroup
+   * @param {any} opts mongoose-paginate options object
+   * @returns {Promise<any>} mongoose-paginate result object
+   * @memberof UserGroupRelation
+   */
+  static findPageGroupRelationsWithPagination(userGroup, opts) {
+    const query = { relatedGroup: userGroup };
+    const options = Object.assign({}, opts);
+    if (options.page == null) {
+      options.page = 1;
+    }
+    if (options.limit == null) {
+      options.limit = UserGroupRelation.PAGE_ITEMS;
+    }
+
+    return this.paginate(query, options)
+      .catch((err) => {
+        debug('Error on pagination:', err);
+      });
+  }
+
+  /**
+   * find the relation or create(if not exists) for page and group
+   *
+   * @static
+   * @param {Page} page
+   * @param {UserGroup} userGroup
+   * @returns {Promise<PageGroupRelation>}
+   * @memberof PageGroupRelation
+   */
+  static findOrCreateRelationForPageAndGroup(page, userGroup) {
+    const query = { targetPage: page.id, relatedGroup: userGroup.id };
+
+    return this
+      .count(query)
+      .then((count) => {
+        // return (0 < count);
+        if (0 < count) {
+          return this.find(query).exec();
+        }
+        else {
+          return this.createRelation(userGroup, page);
+        }
+      })
+      .catch((err) => {
+        debug('An Error occured.', err);
+        return reject(err);
+      });
+  }
+
+  /**
+   * find page and group relation for Page
+   *
+   * @static
+   * @param {Page} page
+   * @returns {Promise<PageGroupRelation[]>}
+   * @memberof PageGroupRelation
+   */
+  static findByPage(page) {
+
+    return this
+      .find({ targetPage: page.id })
+      .populate('relatedGroup')
+      .exec();
+  }
+
+  /**
+   * get is exists granted group for relatedPage and relatedUser
+   *
+   * @static
+   * @param {any} pageData relatedPage
+   * @param {any} userData relatedUser
+   * @returns is exists granted group(or not)
+   * @memberof PageGroupRelation
+   */
+  static isExistsGrantedGroupForPageAndUser(pageData, userData) {
+    var UserGroupRelation = PageGroupRelation.crowi.model('UserGroupRelation');
+
+    return this.findByPage(pageData)
+      .then((pageRelations) => {
+        return pageRelations.map((pageRelation) => {
+          return UserGroupRelation.isRelatedUserForGroup(userData, pageRelation.relatedGroup)
+        });
+      })
+      .then((checkPromises) => {
+        return Promise.all(checkPromises)
+      })
+      .then((checkResults) => {
+        var checkResult = false;
+        checkResults.map((result) => {
+          if (result) {
+            checkResult = true;
+          }
+        });
+        return checkResult;
+      })
+      .catch((err) => {
+        return reject(err);
+      });
+  }
+
+  /**
+   * create page and group relation
+   *
+   * @static
+   * @param {any} userGroup
+   * @param {any} page
+   * @returns
+   * @memberof PageGroupRelation
+   */
+  static createRelation(userGroup, page) {
+    return this.create({
+      relatedGroup: userGroup.id,
+      targetPage: page.id,
+    });
+  };
+
+  /**
+   * remove all relation for UserGroup
+   *
+   * @static
+   * @param {UserGroup} userGroup related group for remove
+   * @returns {Promise<any>}
+   * @memberof PageGroupRelation
+   */
+  static removeAllByUserGroup(userGroup) {
+
+    return this.findAllRelationForUserGroup(userGroup)
+      .then((relations) => {
+        if (relations == null) {
+          return;
+        }
+        else {
+          relations.map((relation) => {
+            relation.remove();
+          });
+        }
+      });
+  }
+
+  /**
+   * remove all relation for Page
+   *
+   * @static
+   * @param {Page} page related page for remove
+   * @returns {Promise<any>}
+   * @memberof PageGroupRelation
+   */
+  static removeAllByPage(page) {
+
+    return this.findByPage(page)
+      .then((relations) => {
+        debug('remove relations are ', relations);
+        if (relations == null) {
+          return;
+        }
+        else {
+          relations.map((relation) => {
+            relation.remove();
+          });
+        }
+      });
+  }
+
+  /**
+   * remove relation by id
+   *
+   * @static
+   * @param {ObjectId} id for remove
+   * @returns {Promise<any>}
+   * @memberof PageGroupRelation
+   */
+  static removeById(id) {
+
+    return this.findById(id)
+      .then((relationData) => {
+        if (relationData == null) {
+          throw new Exception('PageGroupRelation data is not exists. id:', id);
+        }
+        else {
+          relationData.remove();
+        }
+      })
+      .catch((err) => {
+        debug('Error on find a removing page-group-relation', err);
+        return reject(err);
+      });
+  }
+}
+
+module.exports = function (crowi) {
+  PageGroupRelation.crowi = crowi;
+  schema.loadClass(PageGroupRelation);
+  return mongoose.model('PageGroupRelation', schema);
+}

+ 75 - 11
lib/models/page.js

@@ -7,6 +7,7 @@ module.exports = function(crowi) {
     , GRANT_RESTRICTED = 2
     , GRANT_SPECIFIED = 3
     , GRANT_OWNER = 4
+    , GRANT_USER_GROUP = 5
     , PAGE_GRANT_ERROR = 1
 
     , STATUS_WIP        = 'wip'
@@ -340,6 +341,7 @@ module.exports = function(crowi) {
     grantLabels[GRANT_PUBLIC]     = 'Public'; // 公開
     grantLabels[GRANT_RESTRICTED] = 'Anyone with the link'; // リンクを知っている人のみ
     //grantLabels[GRANT_SPECIFIED]  = 'Specified users only'; // 特定ユーザーのみ
+    grantLabels[GRANT_USER_GROUP] = 'Only inside the group'; // 特定グループのみ
     grantLabels[GRANT_OWNER]      = 'Just me'; // 自分のみ
 
     return grantLabels;
@@ -454,15 +456,26 @@ module.exports = function(crowi) {
 
   pageSchema.statics.findPageByIdAndGrantedUser = function(id, userData) {
     var Page = this;
+    var PageGroupRelation = crowi.model('PageGroupRelation');
+    var pageData = null;
 
     return new Promise(function(resolve, reject) {
       Page.findPageById(id)
-      .then(function(pageData) {
+      .then(function(result) {
+        pageData = result;
         if (userData && !pageData.isGrantedFor(userData)) {
+          return PageGroupRelation.isExistsGrantedGroupForPageAndUser(pageData, userData);
+        }
+        else {
+          return true;
+        }
+      }).then((checkResult) => {
+        if (checkResult) {
+          return resolve(pageData);
+        }
+        else  {
           return reject(new Error('Page is not granted for the user')); //PAGE_GRANT_ERROR, null);
         }
-
-        return resolve(pageData);
       }).catch(function(err) {
         return reject(err);
       });
@@ -472,6 +485,7 @@ module.exports = function(crowi) {
   // find page and check if granted user
   pageSchema.statics.findPage = function(path, userData, revisionId, ignoreNotFound) {
     var self = this;
+    var PageGroupRelation = crowi.model('PageGroupRelation');
 
     return new Promise(function(resolve, reject) {
       self.findOne({path: path}, function(err, pageData) {
@@ -490,10 +504,22 @@ module.exports = function(crowi) {
         }
 
         if (!pageData.isGrantedFor(userData)) {
-          return reject(new Error('Page is not granted for the user')); //PAGE_GRANT_ERROR, null);
+          PageGroupRelation.isExistsGrantedGroupForPageAndUser(pageData, userData)
+            .then(function (checkResult) {
+              if (!checkResult) {
+                return reject(new Error('Page is not granted for the user')); //PAGE_GRANT_ERROR, null);
+              } else {
+                // return resolve(pageData);
+                self.populatePageData(pageData, revisionId || null).then(resolve).catch(reject);
+              }
+            })
+            .catch(function (err) {
+              return reject(err);
+            });
+        }
+        else {
+          self.populatePageData(pageData, revisionId || null).then(resolve).catch(reject);
         }
-
-        self.populatePageData(pageData, revisionId || null).then(resolve).catch(reject);
       });
     });
   };
@@ -758,12 +784,15 @@ module.exports = function(crowi) {
     });
   };
 
-  pageSchema.statics.updateGrant = function(page, grant, userData) {
+  pageSchema.statics.updateGrant = function (page, grant, userData, grantUserGroupId) {
     var Page = this;
 
+    if (grant == GRANT_USER_GROUP && grantUserGroupId == null) {
+      throw new Error('grant userGroupId is not specified');
+    }
     return new Promise(function(resolve, reject) {
       page.grant = grant;
-      if (grant == GRANT_PUBLIC) {
+      if (grant == GRANT_PUBLIC || grant == GRANT_USER_GROUP) {
         page.grantedUsers = [];
       } else {
         page.grantedUsers = [];
@@ -776,11 +805,38 @@ module.exports = function(crowi) {
           return reject(err);
         }
 
-        return resolve(data);
+        Page.updateGrantUserGroup(page, grant, grantUserGroupId, userData)
+        .then(() => {
+          return resolve(data);
+        });
       });
     });
   };
 
+  pageSchema.statics.updateGrantUserGroup = function (page, grant, grantUserGroupId, userData) {
+    var UserGroupRelation = crowi.model('UserGroupRelation');
+    var PageGroupRelation = crowi.model('PageGroupRelation');
+
+    // グループの場合
+    if (grant == GRANT_USER_GROUP) {
+      debug('grant is usergroup', grantUserGroupId);
+      return UserGroupRelation.findByGroupIdAndUser(grantUserGroupId, userData)
+      .then((relation) => {
+        if (relation == null) {
+          return reject(new Error('no relations were exist for group and user.'));
+        }
+        return PageGroupRelation.findOrCreateRelationForPageAndGroup(page, relation.relatedGroup);
+      })
+      .catch((err) => {
+        return reject(new Error('No UserGroup is exists. userGroupId : ', grantUserGroupId));
+      });
+    }
+    else {
+      return PageGroupRelation.removeAllByPage(page);
+    }
+
+  };
+
   // Instance method でいいのでは
   pageSchema.statics.pushToGrantedUsers = function(page, userData) {
 
@@ -837,7 +893,8 @@ module.exports = function(crowi) {
       , Revision = crowi.model('Revision')
       , format = options.format || 'markdown'
       , grant = options.grant || GRANT_PUBLIC
-      , redirectTo = options.redirectTo || null;
+      , redirectTo = options.redirectTo || null
+      , grantUserGroupId = options.grantUserGroupId || null;
 
     // force public
       if (isPortalPath(path)) {
@@ -867,6 +924,12 @@ module.exports = function(crowi) {
               return reject(err);
             }
 
+            if (newPage.grant == Page.GRANT_USER_GROUP && grantUserGroupId != null) {
+              Page.updateGrantUserGroup(newPage, grant, grantUserGroupId, user)
+              .catch((err) => {
+                return reject(err);
+              });
+            }
             var newRevision = Revision.prepareRevision(newPage, body, user, {format: format});
             Page.pushRevision(newPage, newRevision, user).then(function(data) {
               resolve(data);
@@ -884,6 +947,7 @@ module.exports = function(crowi) {
     var Page = this
       , Revision = crowi.model('Revision')
       , grant = options.grant || null
+      , grantUserGroupId = options.grantUserGroupId || null
       ;
     // update existing page
     var newRevision = Revision.prepareRevision(pageData, body, user);
@@ -892,7 +956,7 @@ module.exports = function(crowi) {
       Page.pushRevision(pageData, newRevision, user)
       .then(function(revision) {
         if (grant != pageData.grant) {
-          return Page.updateGrant(pageData, grant, user).then(function(data) {
+          return Page.updateGrant(pageData, grant, user, grantUserGroupId).then(function(data) {
             debug('Page grant update:', data);
             resolve(data);
             pageEvent.emit('update', data, user);

+ 1 - 1
lib/models/revision.js

@@ -14,7 +14,7 @@ module.exports = function(crowi) {
   });
 
   /*
-   * preparation for https://github.com/weseek/crowi-plus/issues/216
+   * preparation for https://github.com/weseek/growi/issues/216
    */
   // // create a XSS Filter instance
   // // TODO read options

+ 279 - 0
lib/models/user-group-relation.js

@@ -0,0 +1,279 @@
+const debug = require('debug')('crowi:models:userGroupRelation');
+const mongoose = require('mongoose');
+const mongoosePaginate = require('mongoose-paginate');
+const ObjectId = mongoose.Schema.Types.ObjectId;
+
+
+/*
+ * define schema
+ */
+const schema = new mongoose.Schema({
+  relatedGroup: { type: ObjectId, ref: 'UserGroup', required: true },
+  relatedUser: { type: ObjectId, ref: 'User', required: true },
+  createdAt: { type: Date, default: Date.now, required: true },
+});
+schema.plugin(mongoosePaginate);
+
+/**
+ * UserGroupRelation Class
+ *
+ * @class UserGroupRelation
+ */
+class UserGroupRelation {
+
+  /**
+   * limit items num for pagination
+   *
+   * @readonly
+   * @static
+   * @memberof UserGroupRelation
+   */
+  static get PAGE_ITEMS() {
+    return 50;
+  }
+
+  static set crowi(crowi) {
+    this._crowi = crowi;
+  }
+
+  static get crowi() {
+    return this._crowi;
+  }
+
+  /**
+   * find all user and group relation
+   *
+   * @static
+   * @returns {Promise<UserGroupRelation[]>}
+   * @memberof UserGroupRelation
+   */
+  static findAllRelation() {
+
+    return this
+      .find()
+      .populate('relatedUser')
+      .populate('relatedGroup')
+      .exec();
+  };
+
+  /**
+   * find all user and group relation of UserGroup
+   *
+   * @static
+   * @param {UserGroup} userGroup
+   * @returns {Promise<UserGroupRelation[]>}
+   * @memberof UserGroupRelation
+   */
+  static findAllRelationForUserGroup(userGroup) {
+    debug('findAllRelationForUserGroup is called', userGroup);
+    var UserGroupRelation = this;
+
+    return this
+      .find({ relatedGroup: userGroup })
+      .populate('relatedUser')
+      .exec();
+  }
+
+  /**
+   * find all user and group relation of UserGroups
+   *
+   * @static
+   * @param {UserGroup[]} userGroups
+   * @returns {Promise<UserGroupRelation[]>}
+   * @memberof UserGroupRelation
+   */
+  static findAllRelationForUserGroups(userGroups) {
+
+    return this
+      .find({ relatedGroup: { $in: userGroups } })
+      .populate('relatedUser')
+      .exec();
+  }
+
+  /**
+   * find all user and group relation of User
+   *
+   * @static
+   * @param {User} user
+   * @returns {Promise<UserGroupRelation[]>}
+   * @memberof UserGroupRelation
+   */
+  static findAllRelationForUser(user) {
+
+    return this
+      .find({ relatedUser: user.id })
+      .populate('relatedGroup')
+      .exec();
+  }
+
+  /**
+   * find all entities with pagination
+   *
+   * @see https://github.com/edwardhotchkiss/mongoose-paginate
+   *
+   * @static
+   * @param {UserGroup} userGroup
+   * @param {any} opts mongoose-paginate options object
+   * @returns {Promise<any>} mongoose-paginate result object
+   * @memberof UserGroupRelation
+   */
+  static findUserGroupRelationsWithPagination(userGroup, opts) {
+    const query = { relatedGroup: userGroup };
+    const options = Object.assign({}, opts);
+    if (options.page == null) {
+      options.page = 1;
+    }
+    if (options.limit == null) {
+      options.limit = UserGroupRelation.PAGE_ITEMS;
+    }
+
+    return this.paginate(query, options)
+      .catch((err) => {
+        debug('Error on pagination:', err);
+      });
+  }
+
+  /**
+   * find one result by related group id and related user
+   *
+   * @static
+   * @param {string} userGroupId find query param for relatedGroup
+   * @param {User} userData find query param for relatedUser
+   * @returns {Promise<UserGroupRelation>}
+   * @memberof UserGroupRelation
+   */
+  static findByGroupIdAndUser(userGroupId, userData) {
+    const query = {
+      relatedGroup: userGroupId,
+      relatedUser: userData.id
+    }
+
+    return this
+      .findOne(query)
+      .populate('relatedUser')
+      .populate('relatedGroup')
+      .exec();
+  }
+
+  /**
+   * find all "not" related user for UserGroup
+   *
+   * @static
+   * @param {UserGroup} userGroup for find users not related
+   * @returns {Promise<User>}
+   * @memberof UserGroupRelation
+   */
+  static findUserByNotRelatedGroup(userGroup) {
+    const User = UserGroupRelation.crowi.model('User');
+
+    return this.findAllRelationForUserGroup(userGroup)
+      .then((relations) => {
+        const relatedUserIds = relations.map((relation) => {
+          return relation.relatedUser.id;
+        });
+        const query = { _id: { $nin: relatedUserIds }, status: User.STATUS_ACTIVE };
+
+        debug("findUserByNotRelatedGroup ", query);
+        return User.find(query).exec();
+      });
+  }
+
+  /**
+   * get if the user has relation for group
+   *
+   * @static
+   * @param {User} userData
+   * @param {UserGroup} userGroup
+   * @returns {Promise<boolean>} is user related for group(or not)
+   * @memberof UserGroupRelation
+   */
+  static isRelatedUserForGroup(userData, userGroup) {
+    const query = {
+      relatedGroup: userGroup.id,
+      relatedUser: userData.id
+    }
+
+    return this
+      .count(query)
+      .exec()
+      .then((count) => {
+        // return true or false of the relation is exists(not count)
+        return (0 < count);
+      })
+      .catch((err) => {
+        debug('An Error occured.', err);
+        reject(err);
+      });
+  }
+
+  /**
+   * create user and group relation
+   *
+   * @static
+   * @param {UserGroup} userGroup
+   * @param {User} user
+   * @returns {Promise<UserGroupRelation>} created relation
+   * @memberof UserGroupRelation
+   */
+  static createRelation(userGroup, user) {
+    return this.create({
+      relatedGroup: userGroup.id,
+      relatedUser: user.id
+    });
+  }
+
+  /**
+   * remove all relation for UserGroup
+   *
+   * @static
+   * @param {UserGroup} userGroup related group for remove
+   * @returns {Promise<any>}
+   * @memberof UserGroupRelation
+   */
+  static removeAllByUserGroup(userGroup) {
+
+    return this.findAllRelationForUserGroup(userGroup)
+      .then((relations) => {
+        if (relations == null) {
+          return;
+        }
+        else {
+          relations.map((relation) => {
+            relation.remove();
+          });
+        }
+      });
+  }
+
+  /**
+   * remove relation by id
+   *
+   * @static
+   * @param {ObjectId} id
+   * @returns {Promise<any>}
+   * @memberof UserGroupRelation
+   */
+  static removeById(id) {
+
+    return this.findById(id)
+      .then((relationData) => {
+        if (relationData == null) {
+          throw new Exception('UserGroupRelation data is not exists. id:', id);
+        }
+        else {
+          relationData.remove();
+        }
+      })
+      .catch((err) => {
+        debug('Error on find a removing user-group-relation', err);
+        reject(err);
+      });
+  }
+
+}
+
+module.exports = function (crowi) {
+  UserGroupRelation.crowi = crowi;
+  schema.loadClass(UserGroupRelation);
+  return mongoose.model('UserGroupRelation', schema);
+}

+ 151 - 0
lib/models/user-group.js

@@ -0,0 +1,151 @@
+const debug = require('debug')('crowi:models:userGroup');
+const mongoose = require('mongoose');
+const mongoosePaginate = require('mongoose-paginate');
+const ObjectId = mongoose.Schema.Types.ObjectId;
+
+
+/*
+ * define schema
+ */
+const schema = new mongoose.Schema({
+  userGroupId: String,
+  image: String,
+  name: { type: String, required: true, unique: true },
+  createdAt: { type: Date, default: Date.now },
+});
+schema.plugin(mongoosePaginate);
+
+class UserGroup {
+
+  /**
+   * public fields for UserGroup model
+   *
+   * @readonly
+   * @static
+   * @memberof UserGroup
+   */
+  static get USER_GROUP_PUBLIC_FIELDS() {
+    return '_id image name createdAt';
+  }
+
+  /**
+   * limit items num for pagination
+   *
+   * @readonly
+   * @static
+   * @memberof UserGroup
+   */
+  static get PAGE_ITEMS() {
+    return 10;
+  }
+
+  /*
+   * model static methods
+   */
+
+   // グループ画像パスの生成
+  static createUserGroupPictureFilePath(userGroup, name) {
+    var ext = '.' + name.match(/(.*)(?:\.([^.]+$))/)[2];
+
+    return 'userGroup/' + userGroup._id + ext;
+  };
+
+  // すべてのグループを取得(オプション指定可)
+  static findAllGroups(option) {
+
+    return this.find().exec();
+  };
+
+  /**
+   * find all entities with pagination
+   *
+   * @see https://github.com/edwardhotchkiss/mongoose-paginate
+   *
+   * @static
+   * @param {any} opts mongoose-paginate options object
+   * @returns {Promise<any>} mongoose-paginate result object
+   * @memberof UserGroup
+   */
+  static findUserGroupsWithPagination(opts) {
+    const query = {};
+    const options = Object.assign({}, opts);
+    if (options.page == null) {
+      options.page = 1;
+    }
+    if (options.limit == null) {
+      options.limit = UserGroup.PAGE_ITEMS;
+    }
+
+    return this.paginate(query, options)
+      .catch((err) => {
+        debug('Error on pagination:', err);
+      });
+  };
+
+  // TBD: グループ名によるグループ検索
+  static findUserGroupByName(name) {
+    const query = { name: name };
+    return this.findOne(query);
+  };
+
+  // 登録可能グループ名確認
+  static isRegisterableName(name) {
+    const query = { name: name };
+
+    return this.findOne(query)
+      .then((userGroupData) => {
+        return (userGroupData == null);
+      });
+  };
+
+  // グループの完全削除
+  static removeCompletelyById(id) {
+
+    return this.findById(id)
+      .then((userGroupData) => {
+        if (userGroupData == null) {
+          throw new Exception('UserGroup data is not exists. id:', id);
+        }
+        else {
+          return userGroupData.remove();
+        }
+      });
+  }
+
+  // グループ生成(名前が要る)
+  static createGroupByName(name) {
+
+    return this.create({name: name});
+  }
+
+  /*
+   * instance methods
+   */
+
+  // グループ画像の更新
+  updateImage(image) {
+    this.image = image;
+    return this.save();
+  }
+
+  // グループ画像の削除
+  deleteImage() {
+    return this.updateImage(null);
+  }
+
+  // グループ名の更新
+  updateName(name) {
+    // 名前を設定して更新
+    this.name = name;
+    return this.save();
+  }
+
+}
+
+
+module.exports = function (crowi) {
+  UserGroup.crowi = crowi;
+  schema.loadClass(UserGroup);
+  return mongoose.model('UserGroup', schema);
+}
+

+ 3 - 2
lib/models/user.js

@@ -565,6 +565,7 @@ module.exports = function(crowi) {
   userSchema.statics.createUsersByInvitation = function(emailList, toSendEmail, callback) {
     var User = this
       , createdUserList = []
+      , Config = crowi.model('Config')
       , config = crowi.getConfig()
       , mailer = crowi.getMailer()
       ;
@@ -641,13 +642,13 @@ module.exports = function(crowi) {
 
               mailer.send({
                   to: user.email,
-                  subject: 'Invitation to ' + config.crowi['app:title'],
+                  subject: 'Invitation to ' + Config.appTitle(config),
                   template: 'admin/userInvitation.txt',
                   vars: {
                     email: user.email,
                     password: user.password,
                     url: config.crowi['app:url'],
-                    appTitle: config.crowi['app:title'],
+                    appTitle: Config.appTitle(config),
                   }
                 },
                 function (err, s) {

+ 8 - 7
lib/plugins/plugin-utils.js

@@ -11,10 +11,10 @@ class PluginUtils {
    * return a definition objects that has following structure:
    *
    * {
-   *   name: 'crowi-plugin-X',
-   *   meta: require('crowi-plugin-X'),
+   *   name: 'growi-plugin-X',
+   *   meta: require('growi-plugin-X'),
    *   entries: [
-   *     'crowi-plugin-X/lib/client-entry'
+   *     'growi-plugin-X/lib/client-entry'
    *   ]
    * }
    *
@@ -42,13 +42,14 @@ class PluginUtils {
   }
 
   /**
-   * list plugin module objects that starts with 'crowi-plugin-'
+   * list plugin module objects
+   *  that starts with 'growi-plugin-' or 'crowi-plugin-'
    * borrowing from: https://github.com/hexojs/hexo/blob/d1db459c92a4765620343b95789361cbbc6414c5/lib/hexo/load_plugins.js#L17
    *
    * @returns array of objects
    *   [
-   *     { name: 'crowi-plugin-...', version: '1.0.0' },
-   *     { name: 'crowi-plugin-...', version: '1.0.0' },
+   *     { name: 'growi-plugin-...', version: '1.0.0' },
+   *     { name: 'growi-plugin-...', version: '1.0.0' },
    *     ...
    *   ]
    *
@@ -69,7 +70,7 @@ class PluginUtils {
 
     let objs = {};
     Object.keys(deps).forEach((name) => {
-      if (/^crowi-plugin-/.test(name)) {
+      if (/^(crowi|growi)-plugin-/.test(name)) {
         objs[name] = deps[name];
       }
     });

+ 315 - 2
lib/routes/admin.js

@@ -2,16 +2,20 @@ module.exports = function(crowi, app) {
   'use strict';
 
   var debug = require('debug')('crowi:routes:admin')
+    , fs = require('fs')
     , models = crowi.models
     , Page = models.Page
+    , PageGroupRelation = models.PageGroupRelation
     , User = models.User
     , ExternalAccount = models.ExternalAccount
+    , UserGroup = models.UserGroup
+    , UserGroupRelation = models.UserGroupRelation
     , Config = models.Config
     , PluginUtils = require('../plugins/plugin-utils')
     , pluginUtils = new PluginUtils()
     , ApiResponse = require('../util/apiResponse')
 
-    , MAX_PAGE_LIST = 5
+    , MAX_PAGE_LIST = 50
     , actions = {};
 
   function createPager(total, limit, page, pagesCount, maxPageList) {
@@ -395,7 +399,7 @@ module.exports = function(crowi, app) {
     User.findById(id, function(err, userData) {
       userData.statusActivate(function(err, userData) {
         if (err === null) {
-          req.flash('successMessage', userData.name + 'さんのアカウントを承認しました');
+          req.flash('successMessage', userData.name + 'さんのアカウントを有効化しました');
         } else {
           req.flash('errorMessage', '更新に失敗しました。');
           debug(err, userData);
@@ -527,6 +531,315 @@ module.exports = function(crowi, app) {
       });
   };
 
+  actions.userGroup = {};
+  actions.userGroup.index = function (req, res) {
+    var page = parseInt(req.query.page) || 1;
+    var renderVar = {
+      userGroups : [],
+      userGroupRelations : new Map(),
+      pager : null,
+    }
+
+    UserGroup.findUserGroupsWithPagination({ page: page })
+      .then((result) => {
+        const pager = createPager(result.total, result.limit, result.page, result.pages, MAX_PAGE_LIST);
+        var userGroups = result.docs;
+        renderVar.userGroups = userGroups;
+        renderVar.pager = pager;
+        return userGroups.map((userGroup) => {
+          return new Promise((resolve, reject) => {
+            UserGroupRelation.findAllRelationForUserGroup(userGroup)
+            .then((relations) => {
+              return resolve([userGroup, relations]);
+            });
+          });
+        });
+      })
+      .then((allRelationsPromise) => {
+        return Promise.all(allRelationsPromise)
+      })
+      .then((relations) => {
+        renderVar.userGroupRelations = new Map(relations);
+        debug("in findUserGroupsWithPagination findAllRelationForUserGroupResult", renderVar.userGroupRelations);
+        return res.render('admin/user-groups', renderVar);
+      })
+      .catch( function(err) {
+          debug('Error on find all relations', err);
+          return res.json(ApiResponse.error('Error'));
+      });
+  };
+
+  // グループ詳細
+  actions.userGroup.detail = function (req, res) {
+    var name = req.params.name;
+    var renderVar = {
+      userGroup: null,
+      userGroupRelations: [],
+      pageGroupRelations: [],
+      notRelatedusers: []
+    }
+    var targetUserGroup = null;
+    UserGroup.findUserGroupByName(name)
+      .then(function (userGroup) {
+        targetUserGroup = userGroup;
+        if (targetUserGroup == null) {
+          req.flash('errorMessage', 'グループがありません');
+          throw new Error('no userGroup is exists. ', name);
+        }
+        else {
+          renderVar.userGroup = targetUserGroup;
+
+          return Promise.all([
+            // get all user and group relations
+            UserGroupRelation.findAllRelationForUserGroup(targetUserGroup),
+            // get all page and group relations
+            PageGroupRelation.findAllRelationForUserGroup(targetUserGroup),
+            // get all not related users for group
+            UserGroupRelation.findUserByNotRelatedGroup(targetUserGroup),
+          ]);
+        }
+      })
+      .then((resolves) => {
+        renderVar.userGroupRelations = resolves[0];
+        renderVar.pageGroupRelations = resolves[1];
+        renderVar.notRelatedusers = resolves[2];
+        debug('notRelatedusers', renderVar.notRelatedusers);
+
+        return res.render('admin/user-group-detail', renderVar);
+      })
+      .catch((err) => {
+        req.flash('errorMessage', 'ユーザグループの検索に失敗しました');
+        debug('Error on get userGroupDetail', err);
+        return res.redirect('/admin/user-groups');
+      });
+  }
+
+  //グループの生成
+  actions.userGroup.create = function (req, res) {
+    var form = req.form.createGroupForm;
+    if (req.form.isValid) {
+      UserGroup.createGroupByName(form.userGroupName)
+      .then((newUserGroup) => {
+        req.flash('successMessage', newUserGroup.name)
+        req.flash('createdUserGroup', newUserGroup);
+        return res.redirect('/admin/user-groups');
+      })
+      .catch((err) => {
+        debug('create userGroup error:', err);
+        req.flash('errorMessage', '同じグループ名が既に存在します。');
+      });
+    } else {
+      req.flash('errorMessage', req.form.errors.join('\n'));
+      return res.redirect('/admin/user-groups');
+    }
+  };
+
+  //
+  actions.userGroup.update = function (req, res) {
+
+    var userGroupId = req.params.userGroupId;
+    var name = req.body.name;
+
+    UserGroup.findById(userGroupId)
+    .then((userGroupData) => {
+      if (userGroupData == null) {
+        req.flash('errorMessage', 'グループの検索に失敗しました。');
+        return new Promise();
+      }
+      else {
+        // 名前存在チェック
+        return UserGroup.isRegisterableName(name)
+        .then((isRegisterableName) => {
+          // 既に存在するグループ名に更新しようとした場合はエラー
+          if (!isRegisterableName) {
+            req.flash('errorMessage', 'グループ名が既に存在します。');
+          }
+          else {
+            return userGroupData.updateName(name)
+            .then(() => {
+              req.flash('successMessage', 'グループ名を更新しました。');
+            })
+            .catch((err) => {
+              req.flash('errorMessage', 'グループ名の更新に失敗しました。');
+            });
+          }
+        });
+      }
+    })
+    .then(() => {
+      return res.redirect('/admin/user-group-detail/' + name);
+    });
+  };
+
+  actions.userGroup.uploadGroupPicture = function (req, res) {
+    var fileUploader = require('../util/fileUploader')(crowi, app);
+    //var storagePlugin = new pluginService('storage');
+    //var storage = require('../service/storage').StorageService(config);
+
+    var userGroupId = req.params.userGroupId;
+
+    var tmpFile = req.file || null;
+    if (!tmpFile) {
+      return res.json({
+        'status': false,
+        'message': 'File type error.'
+      });
+    }
+
+    UserGroup.findById(userGroupId, function (err, userGroupData) {
+      if (!userGroupData) {
+        return res.json({
+          'status': false,
+          'message': 'UserGroup error.'
+        });
+      }
+
+      var tmpPath = tmpFile.path;
+      var filePath = UserGroup.createUserGroupPictureFilePath(userGroupData, tmpFile.filename + tmpFile.originalname);
+      var acceptableFileType = /image\/.+/;
+
+      if (!tmpFile.mimetype.match(acceptableFileType)) {
+        return res.json({
+          'status': false,
+          'message': 'File type error. Only image files is allowed to set as user picture.',
+        });
+      }
+
+      var tmpFileStream = fs.createReadStream(tmpPath, { flags: 'r', encoding: null, fd: null, mode: '0666', autoClose: true });
+
+      fileUploader.uploadFile(filePath, tmpFile.mimetype, tmpFileStream, {})
+        .then(function (data) {
+          var imageUrl = fileUploader.generateUrl(filePath);
+          userGroupData.updateImage(imageUrl)
+          .then(() => {
+            fs.unlink(tmpPath, function (err) {
+              if (err) {
+                debug('Error while deleting tmp file.', err);
+              }
+
+              return res.json({
+                'status': true,
+                'url': imageUrl,
+                'message': '',
+              });
+            });
+          });
+        }).catch(function (err) {
+          debug('Uploading error', err);
+
+          return res.json({
+            'status': false,
+            'message': 'Error while uploading to ',
+          });
+        });
+    });
+
+  };
+
+  actions.userGroup.deletePicture = function (req, res) {
+
+    var userGroupId = req.params.userGroupId;
+    let userGroupName = null;
+
+    UserGroup.findById(userGroupId)
+    .then((userGroupData) => {
+      if (userGroupData == null) {
+        return Promise.reject();
+      }
+      else {
+        userGroupName = userGroupData.name;
+        return userGroupData.deleteImage();
+      }
+    })
+    .then((updated) => {
+      req.flash('successMessage', 'Deleted group picture');
+
+      return res.redirect('/admin/user-group-detail/' + userGroupName);
+    })
+    .catch((err) => {
+      debug('An error occured.', err);
+
+      req.flash('errorMessage', 'Error while deleting group picture');
+      if (userGroupName == null) {
+        return res.redirect('/admin/user-groups/');
+      }
+      else {
+        return res.redirect('/admin/user-group-detail/' + userGroupName);
+      }
+    });
+  };
+
+  // app.post('/_api/admin/user-group/delete' , admin.userGroup.removeCompletely);
+  actions.userGroup.removeCompletely = function (req, res) {
+    const id = req.body.user_group_id;
+
+    UserGroup.removeCompletelyById(id)
+    .then(() => {
+        req.flash('successMessage', '削除しました');
+      return res.redirect('/admin/user-groups');
+    })
+    .catch((err) => {
+      debug('Error while removing userGroup.', err, id);
+      req.flash('errorMessage', '完全な削除に失敗しました。');
+      return res.redirect('/admin/user-groups');
+    });
+  }
+
+  actions.userGroupRelation = {};
+  actions.userGroupRelation.index = function(req, res) {
+
+  }
+
+  actions.userGroupRelation.create = function(req, res) {
+    const User = crowi.model('User');
+    const UserGroup = crowi.model('UserGroup');
+    const UserGroupRelation = crowi.model('UserGroupRelation');
+
+    // req params
+    const userName = req.body.user_name;
+    const userGroupId = req.body.user_group_id;
+
+    let user = null;
+    let userGroup = null;
+
+    Promise.all([
+      // ユーザグループをIDで検索
+      UserGroup.findById(userGroupId),
+      // ユーザを名前で検索
+      User.findUserByUsername(userName),
+    ])
+    .then((resolves) => {
+      userGroup = resolves[0];
+      user = resolves[1];
+      // Relation を作成
+      UserGroupRelation.createRelation(userGroup, user)
+    })
+    .then((result) => {
+      return res.redirect('/admin/user-group-detail/' + userGroup.name);
+    }).catch((err) => {
+      debug('Error on create user-group relation', err);
+      req.flash('errorMessage', 'Error on create user-group relation');
+          return res.redirect('/admin/user-group-detail/' + userGroup.name);
+    });
+  }
+
+  actions.userGroupRelation.remove = function (req, res) {
+    const UserGroupRelation = crowi.model('UserGroupRelation');
+    var name = req.params.name;
+    var relationId = req.params.relationId;
+
+    debug(name, relationId);
+    UserGroupRelation.removeById(relationId)
+    .then(() =>{
+      return res.redirect('/admin/user-group-detail/' + name);
+    })
+    .catch((err) => {
+      debug('Error on remove user-group-relation', err);
+      req.flash('errorMessage', 'グループのユーザ削除に失敗しました。');
+    });
+
+  }
+
   actions.api = {};
   actions.api.appSetting = function(req, res) {
     var form = req.form.settingForm;

+ 15 - 0
lib/routes/index.js

@@ -74,6 +74,8 @@ module.exports = function(crowi, app) {
   app.post('/_api/admin/customize/css'      , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.customcss, admin.api.customizeSetting);
   app.post('/_api/admin/customize/script'   , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.customscript, admin.api.customizeSetting);
   app.post('/_api/admin/customize/header'   , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.customheader, admin.api.customizeSetting);
+  app.post('/_api/admin/customize/theme'    , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.customtheme, admin.api.customizeSetting);
+  app.post('/_api/admin/customize/title'    , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.customtitle, admin.api.customizeSetting);
   app.post('/_api/admin/customize/behavior' , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.custombehavior, admin.api.customizeSetting);
   app.post('/_api/admin/customize/layout'   , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.customlayout, admin.api.customizeSetting);
   app.post('/_api/admin/customize/features' , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.customfeatures, admin.api.customizeSetting);
@@ -107,6 +109,19 @@ module.exports = function(crowi, app) {
   app.get('/admin/users/external-accounts'               , loginRequired(crowi, app) , middleware.adminRequired() , admin.externalAccount.index);
   app.post('/admin/users/external-accounts/:id/remove'   , loginRequired(crowi, app) , middleware.adminRequired() , admin.externalAccount.remove);
 
+  // user-groups admin
+  app.get('/admin/user-groups'             , loginRequired(crowi, app), middleware.adminRequired(), admin.userGroup.index);
+  app.get('/admin/user-group-detail/:name'          , loginRequired(crowi, app), middleware.adminRequired(), admin.userGroup.detail);
+  app.post('/admin/user-group/create'      , form.admin.userGroupCreate, loginRequired(crowi, app), middleware.adminRequired(), csrf, admin.userGroup.create);
+  app.post('/admin/user-group/:userGroupId/update', loginRequired(crowi, app), middleware.adminRequired(), csrf, admin.userGroup.update);
+  app.post('/admin/user-group/:userGroupId/picture/delete', loginRequired(crowi, app), admin.userGroup.deletePicture);
+  app.post('/admin/user-group.remove' , loginRequired(crowi, app), middleware.adminRequired(), csrf, admin.userGroup.removeCompletely);
+  app.post('/_api/admin/user-group/:userGroupId/picture/upload', loginRequired(crowi, app), uploads.single('userGroupPicture'), admin.userGroup.uploadGroupPicture);
+
+  // user-group-relations admin
+  app.post('/admin/user-group-relation/create', loginRequired(crowi, app), middleware.adminRequired(), csrf, admin.userGroupRelation.create)
+  app.post('/admin/user-group-relation/:name/remove-relation/:relationId', loginRequired(crowi, app), middleware.adminRequired(), csrf, admin.userGroupRelation.remove)
+
   app.get('/me'                       , loginRequired(crowi, app) , me.index);
   app.get('/me/password'              , loginRequired(crowi, app) , me.password);
   app.get('/me/apiToken'              , loginRequired(crowi, app) , me.apiToken);

+ 9 - 5
lib/routes/installer.js

@@ -37,14 +37,18 @@ module.exports = function(crowi, app) {
               return ;
             }
 
-            // login処理
-            req.user = req.session.user = userData;
-            req.flash('successMessage', 'Crowi のインストールが完了しました!はじめに、このページでこの Wiki の各種設定を確認してください。');
-            return res.redirect('/admin/app');
+            // login with passport
+            req.logIn(userData, (err) => {
+              if (err) { return next(); }
+              else {
+                req.flash('successMessage', 'GROWI のインストールが完了しました!はじめに、このページで各種設定を確認してください。');
+                return res.redirect('/admin/app');
+              }
+            });
           });
 
           // create portal page for '/'
-          Page.create('/', '# Welcome to crowi-plus!', userData, {});
+          Page.create('/', '# Welcome to GROWI', userData, {});
         });
       });
     } else {

+ 3 - 2
lib/routes/login.js

@@ -193,6 +193,7 @@ module.exports = function(crowi, app) {
           } else {
 
             // 作成後、承認が必要なモードなら、管理者に通知する
+            const appTitle = Config.appTitle(config);
             if (config.crowi['security:registrationMode'] === Config.SECURITY_REGISTRATION_MODE_RESTRICTED) {
               // TODO send mail
               User.findAdmins(function(err, admins) {
@@ -201,13 +202,13 @@ module.exports = function(crowi, app) {
                   function(adminUser, next) {
                     mailer.send({
                         to: adminUser.email,
-                        subject: '[' + config.crowi['app:title'] + ':admin] A New User Created and Waiting for Activation',
+                        subject: '[' + appTitle + ':admin] A New User Created and Waiting for Activation',
                         template: 'admin/userWaitingActivation.txt',
                         vars: {
                           createdUser: userData,
                           adminUser: adminUser,
                           url: config.crowi['app:url'],
-                          appTitle: config.crowi['app:title'],
+                          appTitle: appTitle,
                         }
                       },
                       function (err, s) {

+ 44 - 25
lib/routes/page.js

@@ -8,6 +8,7 @@ module.exports = function(crowi, app) {
     , config   = crowi.getConfig()
     , Revision = crowi.model('Revision')
     , Bookmark = crowi.model('Bookmark')
+    , UserGroupRelation = crowi.model('UserGroupRelation')
     , ApiResponse = require('../util/apiResponse')
     , interceptorManager = crowi.getInterceptorManager()
 
@@ -69,11 +70,11 @@ module.exports = function(crowi, app) {
   actions.pageListShowWrapper = function(req, res) {
     const behaviorType = Config.behaviorType(config);
 
-    if ('crowi-plus' === behaviorType) {
-      return actions.pageListShowForCrowiPlus(req, res);
+    if (!behaviorType || 'crowi' === behaviorType) {
+      return actions.pageListShow(req, res);
     }
     else {
-      return actions.pageListShow(req, res);
+      return actions.pageListShowForCrowiPlus(req, res);
     }
   }
   /**
@@ -82,11 +83,11 @@ module.exports = function(crowi, app) {
   actions.pageShowWrapper = function(req, res) {
     const behaviorType = Config.behaviorType(config);
 
-    if ('crowi-plus' === behaviorType) {
-      return actions.pageShowForCrowiPlus(req, res);
+    if (!behaviorType || 'crowi' === behaviorType) {
+      return actions.pageShow(req, res);
     }
     else {
-      return actions.pageShow(req, res);
+      return actions.pageShowForCrowiPlus(req, res);
     }
   }
   /**
@@ -95,13 +96,13 @@ module.exports = function(crowi, app) {
   actions.trashPageListShowWrapper = function(req, res) {
     const behaviorType = Config.behaviorType(config);
 
-    if ('crowi-plus' === behaviorType) {
-      // redirect to '/trash'
-      return res.redirect('/trash');
+    if (!behaviorType || 'crowi' === behaviorType) {
+      // Crowi behavior for '/trash/*'
+      return actions.deletedPageListShow(req, res);
     }
-    // official Crowi behavior for '/trash/*'
     else {
-      return actions.deletedPageListShow(req, res);
+      // redirect to '/trash'
+      return res.redirect('/trash');
     }
   }
   /**
@@ -110,14 +111,14 @@ module.exports = function(crowi, app) {
   actions.trashPageShowWrapper = function(req, res) {
     const behaviorType = Config.behaviorType(config);
 
-    if ('crowi-plus' === behaviorType) {
-      // official Crowi behavior for '/trash/*'
-      return actions.deletedPageListShow(req, res);
-    }
-    else {
+    if (!behaviorType || 'crowi' === behaviorType) {
       // redirect to '/trash/'
       return res.redirect('/trash/');
     }
+    else {
+      // Crowi behavior for '/trash/*'
+      return actions.deletedPageListShow(req, res);
+    }
 
   }
   /**
@@ -126,13 +127,13 @@ module.exports = function(crowi, app) {
   actions.deletedPageListShowWrapper = function(req, res) {
     const behaviorType = Config.behaviorType(config);
 
-    if ('crowi-plus' === behaviorType) {
-      const path = '/trash' + getPathFromRequest(req);
-      return res.redirect(path);
+    if (!behaviorType || 'crowi' === behaviorType) {
+      // Crowi behavior for '/trash/*'
+      return actions.deletedPageListShow(req, res);
     }
-    // official Crowi behavior for '/trash/*'
     else {
-      return actions.deletedPageListShow(req, res);
+      const path = '/trash' + getPathFromRequest(req);
+      return res.redirect(path);
     }
   }
 
@@ -160,6 +161,7 @@ module.exports = function(crowi, app) {
     var renderVars = {
       page: null,
       path: path,
+      isPortal: false,
       pages: [],
       tree: [],
     };
@@ -167,6 +169,7 @@ module.exports = function(crowi, app) {
     Page.hasPortalPage(path, req.user, req.query.revision)
     .then(function(portalPage) {
       renderVars.page = portalPage;
+      renderVars.isPortal = (portalPage != null);
 
       if (portalPage) {
         renderVars.revision = portalPage.revision;
@@ -231,6 +234,7 @@ module.exports = function(crowi, app) {
       author: false,
       pages: [],
       tree: [],
+      userRelatedGroups: [],
     };
 
     var pageTeamplate = 'customlayout-selector/page';
@@ -331,6 +335,15 @@ module.exports = function(crowi, app) {
           debug('Error on rendering pageListShowForCrowiPlus', err);
         });
       }
+    })
+    .then(function() {
+      return UserGroupRelation.findAllRelationForUser(req.user);
+    }).then(function (groupRelations) {
+      debug('findPage : relatedGroups ', groupRelations);
+      renderVars.userRelatedGroups = groupRelations.map(relation => relation.relatedGroup);
+      debug('findPage : groups ', renderVars.userRelatedGroups);
+
+      return Promise.resolve();
     });
   }
 
@@ -406,7 +419,7 @@ module.exports = function(crowi, app) {
   function renderPage(pageData, req, res) {
     // create page
     if (!pageData) {
-      return res.render('page', {
+      return res.render('customlayout-selector/not_found', {
         author: {},
         page: false,
       });
@@ -548,6 +561,7 @@ module.exports = function(crowi, app) {
     var currentRevision = pageForm.currentRevision;
     var grant = pageForm.grant;
     var path = pageForm.path;
+    var grantUserGroupId = pageForm.grantUserGroupId
 
     // TODO: make it pluggable
     var notify = pageForm.notify || {};
@@ -586,11 +600,11 @@ module.exports = function(crowi, app) {
 
       if (data) {
         previousRevision = data.revision;
-        return Page.updatePage(data, body, req.user, {grant: grant});
+        return Page.updatePage(data, body, req.user, { grant: grant, grantUserGroupId: grantUserGroupId});
       } else {
         // new page
         updateOrCreate = 'create';
-        return Page.create(path, body, req.user, {grant: grant});
+        return Page.create(path, body, req.user, { grant: grant, grantUserGroupId: grantUserGroupId});
       }
     }).then(function(data) {
       // data is a saved page data.
@@ -788,6 +802,7 @@ module.exports = function(crowi, app) {
     var body = req.body.body || null;
     var pagePath = req.body.path || null;
     var grant = req.body.grant || null;
+    var grantUserGroupId = req.body.grantUserGroupId || null;
 
     if (body === null || pagePath === null) {
       return res.json(ApiResponse.error('Parameters body and path are required.'));
@@ -800,7 +815,7 @@ module.exports = function(crowi, app) {
         throw new Error('Page exists');
       }
 
-      return Page.create(pagePath, body, req.user, {grant: grant});
+      return Page.create(pagePath, body, req.user, { grant: grant, grantUserGroupId: grantUserGroupId});
     }).then(function(data) {
       if (!data) {
         throw new Error('Failed to create page.');
@@ -835,6 +850,7 @@ module.exports = function(crowi, app) {
     var pageId = req.body.page_id || null;
     var revisionId = req.body.revision_id || null;
     var grant = req.body.grant || null;
+    var grantUserGroupId = req.body.grantUserGroupId || null;
 
     if (pageId === null || pageBody === null) {
       return res.json(ApiResponse.error('page_id and body are required.'));
@@ -850,6 +866,9 @@ module.exports = function(crowi, app) {
       if (grant !== null) {
         grantOption.grant = grant;
       }
+      if (grantUserGroupId != null) {
+        grantOption.grantUserGroupId = grantUserGroupId;
+      }
       return Page.updatePage(pageData, pageBody, req.user, grantOption);
     }).then(function(pageData) {
       var result = {

+ 2 - 1
lib/util/mailer.js

@@ -8,6 +8,7 @@ module.exports = function(crowi) {
   var debug = require('debug')('crowi:lib:mailer')
     , nodemailer = require('nodemailer')
     , swig = require('swig-templates')
+    , Config = crowi.model('Config')
     , config = crowi.getConfig()
     , mailConfig = {}
     , mailer = {}
@@ -81,7 +82,7 @@ module.exports = function(crowi) {
     }
 
     mailConfig.from = config.crowi['mail:from'];
-    mailConfig.subject = config.crowi['app:title'] + 'からのメール';
+    mailConfig.subject = Config.appTitle(config)  + 'からのメール';
 
     debug('mailer initialized');
   }

+ 2 - 2
lib/util/middlewares.js

@@ -89,7 +89,7 @@ exports.swigFilters = function(app, swig) {
       return user.image;
     }
     else {
-      return '/images/userpicture.png';
+      return '/images/icons/user.svg';
     }
   };
 
@@ -165,7 +165,7 @@ exports.swigFilters = function(app, swig) {
 
     swig.setFilter('picture', function(user) {
       if (!user) {
-        return '/images/userpicture.png';
+        return '/images/icons/user.svg';
       }
 
       if (user.isGravatarEnabled === true) {

+ 1 - 1
lib/util/search.js

@@ -244,7 +244,7 @@ SearchClient.prototype.addAllPages = function()
       // all done
 
       // return if body is empty
-      // see: https://github.com/weseek/crowi-plus/issues/228
+      // see: https://github.com/weseek/growi/issues/228
       if (body.length == 0) {
         return resolve();
       }

+ 1 - 1
lib/util/slack.js

@@ -206,7 +206,7 @@ module.exports = function(crowi) {
 
     var message = {
       channel: '#' + channel,
-      username: config.crowi['app:title'],
+      username: Config.appTitle(config),
       text: this.getSlackMessageText(page.path, user, updateType),
       attachments: [attachment],
     };

+ 42 - 3
lib/util/swigFunctions.js

@@ -16,7 +16,7 @@ module.exports = function(crowi, app, req, locals) {
     return crowi.runtimeVersions.versions.yarn ? crowi.runtimeVersions.versions.yarn.version : '-';
   }
 
-  locals.crowiVersion = function() {
+  locals.growiVersion = function() {
     return crowi.version;
   }
 
@@ -25,11 +25,31 @@ module.exports = function(crowi, app, req, locals) {
     return req.csrfToken;
   };
 
+  locals.getAppTitleFontSize = function(appTitle) {
+    let fontSize = 22;
+    if (appTitle.length < 13) { /* do nothing */ }
+    else if (appTitle.length < 21) {
+      fontSize -= 3 * (Math.floor((appTitle.length - 13) / 3) + 1);
+    }
+    else  {
+      fontSize = 11;
+    }
+    return fontSize;
+  }
+
+  /**
+   * return app title
+   */
+  locals.appTitle = function() {
+    var config = crowi.getConfig();
+    return Config.appTitle(config);
+  }
+
   /**
    * return true if enabled
    */
   locals.isEnabledPassport = function() {
-    var config = crowi.getConfig()
+    var config = crowi.getConfig();
     return Config.isEnabledPassport(config);
   }
 
@@ -58,6 +78,12 @@ module.exports = function(crowi, app, req, locals) {
   }
 
   locals.googleLoginEnabled = function() {
+    // return false if Passport is enabled
+    // because official crowi mechanism is not used.
+    if (locals.isEnabledPassport()) {
+      return false;
+    }
+
     var config = crowi.getConfig()
     return config.crowi['google:clientId'] && config.crowi['google:clientSecret'];
   };
@@ -97,8 +123,21 @@ module.exports = function(crowi, app, req, locals) {
     return Config.customHeader(config);
   }
 
-  locals.behaviorType = function() {
+  locals.theme = function() {
     var config = crowi.getConfig()
+    return Config.theme(config);
+  }
+
+  locals.customTitle = function() {
+    var config = crowi.getConfig();
+    var title = Config.customTitle(config);
+    var app_title = Config.appTitle(config);
+    var custom_title = title.replace('{{sitename}}', app_title);
+    return custom_title;
+  }
+
+  locals.behaviorType = function() {
+    var config = crowi.getConfig();
     return Config.behaviorType(config);
   }
 

+ 20 - 18
lib/views/_form.html

@@ -21,25 +21,20 @@
   <input type="hidden" id="form-body" name="pageForm[body]" value="{% if pageForm.body %}{{ pageForm.body }}{% endif %}">
   <input type="hidden" name="pageForm[path]" value="{{ path }}">
   <input type="hidden" name="pageForm[currentRevision]" value="{{ pageForm.currentRevision|default(page.revision._id.toString()) }}">
-  <div class="form-submit-group form-group form-inline">
-    {#<button class="btn btn-default">
-      <i class="fa fa-file-text"></i>
-      ファイルを追加 ...
-    </button>#}
-
-    <div class="pull-left">
+  <div class="page-editor-footer form-submit-group form-group form-inline
+      d-flex align-items-center justify-content-between">
+    <div>
       <div id="page-editor-options-selector"></div>
     </div>
 
-    <div class="pull-right form-inline page-form-setting" id="page-form-setting" data-slack-configured="{{ slackConfigured() }}">
+    <div class="form-inline page-form-setting d-flex align-items-center" id="page-form-setting" data-slack-configured="{{ slackConfigured() }}">
       {% if slackConfigured() %}
-      <span class="input-group extended-setting">
-        <span class="input-group-addon">
-          <label>
-            <i class="fa fa-slack"></i>
-            <input class="" type="checkbox" name="pageForm[notify][slack][on]" value="1">
-          </label>
-        </span>
+      <span class="input-group input-group-sm input-group-slack extended-setting m-r-5">
+        <div class="input-group-addon">
+          <img id="slack-mark-white" src="/images/icons/slack/mark-monochrome_white.svg" width="18" height="18">
+          <img id="slack-mark-black" src="/images/icons/slack/mark-monochrome_black.svg" width="18" height="18">
+          <input class="" type="checkbox" name="pageForm[notify][slack][on]" value="1">
+        </div>
         <input class="form-control" type="text" name="pageForm[notify][slack][channel]" value="{{ page.extended.slack|default('') }}" placeholder="slack-channel-name"
           id="page-form-slack-channel"
           data-toggle="popover"
@@ -54,14 +49,21 @@
       {% if forceGrant %}
       <input type="hidden" name="pageForm[grant]" value="{{ forceGrant }}">
       {% else %}
-      <select name="pageForm[grant]" class="form-control">
+      <select name="pageForm[grant]" class="m-r-5 selectpicker btn-group-sm">
         {% for grantId, grantLabel in consts.pageGrants %}
-        <option value="{{ grantId }}" {% if pageForm.grant|default(page.grant) == grantId %}selected{% endif %}>{{ t(grantLabel) }}</option>
+        <option value="{{ grantId }}" {% if pageForm.grant|default(page.grant) == grantId %}selected{% endif %} {% if grantId == 5 && userRelatedGroups.length == 0 %}disabled{% endif %}>{{ t(grantLabel) }}</option>
+        {% endfor %}
+      </select>
+      {% endif %}
+      {% if userRelatedGroups.length != 0 %}
+      <select name="pageForm[grantUserGroupId]" class="form-control">
+        {% for userGroup in userRelatedGroups %}
+        <option value="{{ userGroup.id }}">{{ userGroup.name }}</option>
         {% endfor %}
       </select>
       {% endif %}
       <input type="hidden" id="edit-form-csrf" name="_csrf" value="{{ csrf() }}">
-      <input type="submit" class="btn btn-primary" id="edit-form-submit" value="{{ t('Update Page') }}" />
+      <button type="submit" class="btn btn-primary btn-submit" id="edit-form-submit">{{ t('Update') }}</button>
     </div>
   </div>
 </form>

+ 8 - 8
lib/views/admin/app.html

@@ -2,7 +2,7 @@
 
 {% block html_title %}{{ t('App settings') }} · {% endblock %}
 
-{% block content_head %}
+{% block content_header %}
 <div class="header-wrap">
   <header id="page-header">
     <h1 class="title" id="">{{ t('App settings') }}</h1>
@@ -38,8 +38,7 @@
         <div class="form-group">
           <label for="settingForm[app:title]" class="col-xs-3 control-label">{{ t('app_setting.Wiki name') }}</label>
           <div class="col-xs-6">
-            <input class="form-control" type="text" name="settingForm[app:title]" value="{{ settingForm['app:title'] }}">
-
+            <input class="form-control" type="text" name="settingForm[app:title]" value="{{ settingForm['app:title'] | default('') }}">
             <p class="help-block">{{ t("app_setting.wiki_change") }}</p>
           </div>
         </div>
@@ -133,7 +132,7 @@
         {{ t("app_setting.No_SMTP_setting") }}<br>
           <br>
 
-          <span class="text-danger"><i class="fa fa-warning"></i> {{ t("app_setting.change_setting") }}</span>
+          <span class="text-danger"><i class="ti-unlink"></i> {{ t("app_setting.change_setting") }}</span>
         </p>
 
         <div class="form-group">
@@ -183,14 +182,15 @@
         <div class="form-group">
           <label for="settingForm[plugin:isEnabledPlugins]" class="col-xs-3 control-label">{{ t('app_setting.Load plugins') }}</label>
           <div class="col-xs-6">
+
             <div class="btn-group btn-toggle" data-toggle="buttons">
-              <label class="btn btn-default {% if settingForm['plugin:isEnabledPlugins'] %}active{% endif %}" data-active-class="primary">
+              <label class="btn btn-default btn-rounded btn-outline {% if settingForm['plugin:isEnabledPlugins'] %}active{% endif %}" data-active-class="primary">
                 <input name="settingForm[plugin:isEnabledPlugins]" value="true" type="radio"
-                    {% if true === settingForm['plugin:isEnabledPlugins'] %}checked{% endif %}> {{ t('app_setting.valid') }}
+                    {% if true === settingForm['plugin:isEnabledPlugins'] %}checked{% endif %}> ON
               </label>
-              <label class="btn btn-default {% if !settingForm['plugin:isEnabledPlugins'] %}active{% endif %}" data-active-class="primary">
+              <label class="btn btn-default btn-rounded btn-outline {% if !settingForm['plugin:isEnabledPlugins'] %}active{% endif %}" data-active-class="default">
                 <input name="settingForm[plugin:isEnabledPlugins]" value="false" type="radio"
-                    {% if !settingForm['plugin:isEnabledPlugins'] %}checked{% endif %}> {{ t('app_setting.invalid') }}
+                    {% if !settingForm['plugin:isEnabledPlugins'] %}checked{% endif %}> OFF
               </label>
             </div>
           </div>

+ 130 - 23
lib/views/admin/customize.html

@@ -2,6 +2,11 @@
 
 {% block html_title %}{{ t('Customize') }} {% endblock %}
 
+{% block style_css_block %}
+  <link rel="stylesheet" href="{{ webpack_asset('style').css }}">
+  <link rel="stylesheet" id="jssDefault" {# append id for theme selector #} href="{{ webpack_asset('style-theme-' + theme()).css }}">
+{% endblock %}
+
 {% block html_additional_headers %}
   {% parent %}
   <!-- CodeMirror -->
@@ -13,7 +18,7 @@
   </style>
 {% endblock %}
 
-{% block content_head %}
+{% block content_header %}
 <div class="header-wrap">
   <header id="page-header">
     <h1 class="title" id="">{{ t('Customize') }} </h1>
@@ -43,15 +48,46 @@
     </div>
     <div class="col-md-9">
 
+      <form action="/_api/admin/customize/theme" method="post" class="form-horizontal" id="customthemeSettingForm" role="form">
+        <fieldset>
+          <legend>{{ t('customize_page.Theme') }}</legend>
+
+          <div id="themeOptions" class="d-flex">
+            <a id="theme-option-default" href="#"
+                class="default {% if 'default' === settingForm['customize:theme'] %}active{% endif %}"
+                onclick="selectTheme('default')"
+                data-theme="{{ webpack_asset('style-theme-default').css }}">
+              {% include 'widget/theme-colorbox.html' %}
+            </a>
+            <a id="theme-option-default-dark" href="#"
+                class="default-dark {% if 'default-dark' === settingForm['customize:theme'] %}active{% endif %}"
+                onclick="selectTheme('default-dark')"
+                data-theme="{{ webpack_asset('style-theme-default-dark').css }}">
+              {% include 'widget/theme-colorbox.html' %}
+            </a>
+          </div>
+
+          <div class="form-group">
+            <div class="col-xs-offset-5 col-xs-6">
+              <input type="hidden" id="hiddenInputTheme" name="settingForm[customize:theme]" value="{{ settingForm['customize:theme'] }}">
+              <input type="hidden" name="_csrf" value="{{ csrf() }}">
+              <button type="submit" class="btn btn-primary">{{ t('Update') }}</button>
+            </div>
+          </div>
+
+        </fieldset>
+      </form>
+
       <form action="/_api/admin/customize/behavior" method="post" class="form-horizontal" id="cutombehaviorSettingForm" role="form">
       <fieldset>
         <legend>{{ t('customize_page.Behavior') }}</legend>
 
+        {% set isBehaviorGrowi = 'growi' === settingForm['customize:behavior'] || 'crowi-plus' === settingForm['customize:behavior'] %}
         <div class="form-group">
           <div class="col-xs-6">
             <h4>
               <input type="radio" name="settingForm[customize:behavior]" value="crowi"
-                  {% if !settingForm['customize:behavior'] || 'crowi' === settingForm['customize:behavior'] %}checked="checked"{% endif %}>
+                  {% if !isBehaviorGrowi %}checked="checked"{% endif %}>
               Official Crowi Behavior
             </h4>
             <ul>
@@ -67,7 +103,7 @@
           <div class="col-xs-6">
             <h4>
               <input type="radio" name="settingForm[customize:behavior]" value="crowi-plus"
-                  {% if 'crowi-plus' === settingForm['customize:behavior'] %}checked="checked"{% endif %}>
+                  {% if isBehaviorGrowi %}checked="checked"{% endif %}>
               crowi-plus Simplified Behavior <small class="text-success">(Recommended)</small>
             </h4>
             <ul>
@@ -93,11 +129,12 @@
       <fieldset>
         <legend>{{ t('customize_page.Layout') }}</legend>
 
+        {% set isLayoutGrowi = 'growi' === settingForm['customize:layout'] || 'crowi-plus' === settingForm['customize:layout'] %}
         <div class="form-group">
           <div class="col-xs-6">
             <h4>
               <input type="radio" name="settingForm[customize:layout]" value="crowi"
-                  {% if !settingForm['customize:layout'] || 'crowi' === settingForm['customize:layout'] %}checked="checked"{% endif %}>
+                  {% if !isLayoutGrowi %}checked="checked"{% endif %}>
               Official Crowi Classic Layout
             </h4>
             <a href="/images/admin/customize/layout-classic.gif" class="ss-container">
@@ -115,7 +152,7 @@
           <div class="col-xs-6">
             <h4>
               <input type="radio" name="settingForm[customize:layout]" value="crowi-plus"
-                  {% if 'crowi-plus' === settingForm['customize:layout'] %}checked="checked"{% endif %}>
+                  {% if isLayoutGrowi %}checked="checked"{% endif %}>
               crowi-plus Enhanced Layout <small class="text-success">(Recommended)</small>
             </h4>
             <a href="/images/admin/customize/layout-crowi-plus.gif" class="ss-container">
@@ -150,13 +187,13 @@
           <label for="settingForm[customize:isEnabledTimeline]" class="col-xs-3 control-label">{{ t('customize_page.Timeline function') }}</label>
           <div class="col-xs-9">
             <div class="btn-group btn-toggle" data-toggle="buttons">
-              <label class="btn btn-default {% if settingForm['customize:isEnabledTimeline'] %}active{% endif %}" data-active-class="primary">
+              <label class="btn btn-default btn-rounded btn-outline {% if settingForm['customize:isEnabledTimeline'] %}active{% endif %}" data-active-class="primary">
                 <input name="settingForm[customize:isEnabledTimeline]" value="true" type="radio"
-                    {% if true === settingForm['customize:isEnabledTimeline'] %}checked{% endif %}> {{ t('Valid') }}
+                    {% if true === settingForm['customize:isEnabledTimeline'] %}checked{% endif %}> ON
               </label>
-              <label class="btn btn-default {% if !settingForm['customize:isEnabledTimeline'] %}active{% endif %}" data-active-class="primary">
+              <label class="btn btn-default btn-rounded btn-outline {% if !settingForm['customize:isEnabledTimeline'] %}active{% endif %}" data-active-class="default">
                 <input name="settingForm[customize:isEnabledTimeline]" value="false" type="radio"
-                    {% if !settingForm['customize:isEnabledTimeline'] %}checked{% endif %}> {{ t('Invalid') }}
+                    {% if !settingForm['customize:isEnabledTimeline'] %}checked{% endif %}> OFF
               </label>
             </div>
 
@@ -174,13 +211,13 @@
           <label for="settingForm[customize:isSavedStatesOfTabChanges]" class="col-xs-3 control-label">{{ t("customize_page.tab_switch") }}</label>
           <div class="col-xs-9">
             <div class="btn-group btn-toggle" data-toggle="buttons">
-              <label class="btn btn-default {% if settingForm['customize:isSavedStatesOfTabChanges'] %}active{% endif %}" data-active-class="primary">
+              <label class="btn btn-default btn-rounded btn-outline {% if settingForm['customize:isSavedStatesOfTabChanges'] %}active{% endif %}" data-active-class="primary">
                 <input name="settingForm[customize:isSavedStatesOfTabChanges]" value="true" type="radio"
-                    {% if true === settingForm['customize:isSavedStatesOfTabChanges'] %}checked{% endif %}> {{ t('Valid') }}
+                    {% if true === settingForm['customize:isSavedStatesOfTabChanges'] %}checked{% endif %}> ON
               </label>
-              <label class="btn btn-default {% if !settingForm['customize:isSavedStatesOfTabChanges'] %}active{% endif %}" data-active-class="primary">
+              <label class="btn btn-default btn-rounded btn-outline {% if !settingForm['customize:isSavedStatesOfTabChanges'] %}active{% endif %}" data-active-class="default">
                 <input name="settingForm[customize:isSavedStatesOfTabChanges]" value="false" type="radio"
-                    {% if !settingForm['customize:isSavedStatesOfTabChanges'] %}checked{% endif %}> {{ t('Invalid') }}
+                    {% if !settingForm['customize:isSavedStatesOfTabChanges'] %}checked{% endif %}> OFF
               </label>
             </div>
 
@@ -207,7 +244,7 @@
           <div class="form-group">
             <label for="settingForm[customize:highlightJsStyle]" class="col-xs-3 control-label">{{ t('customize_page.Theme') }}</label>
             <div class="col-xs-9">
-              <select class="form-control" name="settingForm[customize:highlightJsStyle]" onChange="selectHighlightJsStyle(event)">
+              <select class="form-control selectpicker" name="settingForm[customize:highlightJsStyle]" onChange="selectHighlightJsStyle(event)">
                 {% for key in Object.keys(highlightJsCssSelectorOptions) %}
                   <option value={{key}} {% if key == highlightJsStyle() %} selected {% endif %}>{{highlightJsCssSelectorOptions[key].name}}</option>
                 {% endfor %}
@@ -219,13 +256,13 @@
             <label for="settingForm[customize:highlightJsStyleBorder]" class="col-xs-3 control-label">(TBD) Border</label>
             <div class="col-xs-9">
               <div class="btn-group btn-toggle" data-toggle="buttons">
-                <label class="btn btn-default {% if settingForm['customize:highlightJsStyleBorder'] %}active{% endif %}" data-active-class="primary">
+                <label class="btn btn-default btn-rounded btn-outline {% if settingForm['customize:highlightJsStyleBorder'] %}active{% endif %}" data-active-class="primary">
                   <input name="settingForm[customize:highlightJsStyleBorder]" value="true" type="radio"
-                      {% if true === settingForm['customize:highlightJsStyleBorder'] %}checked{% endif %}> {{ t('Valid') }}
+                      {% if true === settingForm['customize:highlightJsStyleBorder'] %}checked{% endif %}> ON
                 </label>
-                <label class="btn btn-default {% if !settingForm['customize:highlightJsStyleBorder'] %}active{% endif %}" data-active-class="primary">
+                <label class="btn btn-default btn-rounded btn-outline {% if !settingForm['customize:highlightJsStyleBorder'] %}active{% endif %}" data-active-class="default">
                   <input name="settingForm[customize:highlightJsStyleBorder]" value="false" type="radio"
-                      {% if !settingForm['customize:highlightJsStyleBorder'] %}checked{% endif %}> {{ t('Invalid') }}
+                      {% if !settingForm['customize:highlightJsStyleBorder'] %}checked{% endif %}> OFF
                 </label>
               </div>
             </div>
@@ -292,13 +329,55 @@ export  $initHighlight;</code></pre>
         <div class="form-group">
           <div class="col-xs-offset-5 col-xs-6">
             <input type="hidden" name="_csrf" value="{{ csrf() }}">
-            <button type="submit" class="btn btn-primary">更新</button>
+            <button type="submit" class="btn btn-primary">{{ t('Update') }}</button>
           </div>
         </div>
 
       </fieldset>
       </form>
 
+      <form action="/_api/admin/customize/title" method="post" class="form-horizontal" id="customtitleSettingForm" role="form">
+        <fieldset>
+          <legend>カスタムヘッダーTitle</legend>
+
+          <p class="well">
+            ヘッダーの&lt;title&gt;タグのコンテンツをカスタムできる。サイト名を入れたい位置に、&#123;&#123;sitename&#125;&#125;
+            パスを入れたい位置に&#123;&#123;path&#125;&#125;を置くことでそれぞれの値に自動置換されます。それ以外の部分は自由に記述して下さい。<br>
+          </p>
+
+          <p class="help-block">
+            Examples:
+            <pre><code>&#123;&#123;sitename&#125;&#125; hoge - &#123;&#123;path&#125;&#125;</code></pre>
+          </p>
+
+          <p class="help-block">
+            Output:
+            <pre><code>&lt;title&gt;GROWI hoge - /xxx/yyy/zzz/Sandbox&lt;&#047;title&gt;</code></pre>
+          </p>
+
+          <div class="form-group">
+            <div class="col-xs-12">
+              <div id="custom-title-editor"></div>
+              <input type="hidden" id="inputCustomTitle" name="settingForm[customize:title]" value="{{ settingForm['customize:title'] }}">
+            </div>
+            <div class="col-xs-12">
+              <p class="help-block text-right">
+                <i class="fa fa-fw fa-keyboard-o" aria-hidden="true"></i>
+                Ctrl+Space でコード補完
+              </p>
+            </div>
+          </div>
+
+          <div class="form-group">
+            <div class="col-xs-offset-5 col-xs-6">
+              <input type="hidden" name="_csrf" value="{{ csrf() }}">
+              <button type="submit" class="btn btn-primary">更新</button>
+            </div>
+          </div>
+
+        </fieldset>
+        </form>
+
       <form action="/_api/admin/customize/css" method="post" class="form-horizontal" id="cutomcssSettingForm" role="form">
       <fieldset>
         <legend>{{ t('customize_page.Custom CSS') }}</legend>
@@ -354,7 +433,7 @@ export  $initHighlight;</code></pre>
             <dt><code>crowiRenderer</code></dt>
             <dd>Crowi Renderer instance</dd>
             <dt><code>crowiPlugin</code></dt>
-            <dd>crowi-plus plugin manager instance</dd>
+            <dd>GROWI plugin manager instance</dd>
           </dl>
         </p>
         <p class="help-block">
@@ -391,9 +470,14 @@ window.addEventListener('load', (event) => {
 
     </div>
   </div>
+{% endblock content_main %}
 
+{% block body_end %}
+  {% parent %}
   <script>
-    $('#cutomcssSettingForm, #cutomscriptSettingForm, #cutomlayoutSettingForm, #cutombehaviorSettingForm, #customfeaturesSettingForm, #cutomheaderSettingForm, #cutomhighlightJsStyleSettingForm').each(function() {
+    $(`#customthemeSettingForm, #cutomlayoutSettingForm, #cutombehaviorSettingForm, #cutomhighlightJsStyleSettingForm,
+       #customfeaturesSettingForm, #cutomheaderSettingForm, #cutomcssSettingForm, #cutomscriptSettingForm, #customtitleSettingForm`
+    ).each(function() {
       $(this).submit(function()
       {
         function showMessage(formId, msg, status) {
@@ -441,7 +525,9 @@ window.addEventListener('load', (event) => {
       });
     });
 
-    // init highlight.js
+    /*
+     * highlight.js style switcher
+     */
     hljs.initHighlightingOnLoad()
 
     function selectHighlightJsStyle(event) {
@@ -453,10 +539,31 @@ window.addEventListener('load', (event) => {
       highlightJsCssDOM.href = highlightJsCssDOM.href.replace(/[^/]+\.css$/, `${val}.css`);
     }
 
+    /*
+     * Theme Selector
+     */
+    options = {
+      hasPreview: false,
+      fullPath: '',
+      cookie: {
+        isManagingLoad: false
+      }
+    };
+    $(document).ready(function() {
+      $('#themeOptions').styleSwitcher(options);
+    });
+
+    function selectTheme(theme) {
+      // update hidden
+      $('#hiddenInputTheme').val(theme);
+      // update .active class
+      $('#themeOptions .active').removeClass('active');
+      $(`#themeOptions #theme-option-${theme}`).addClass('active');
+    }
   </script>
 
 </div>
-{% endblock content_main %}
+{% endblock %}
 
 {% block content_footer %}
 {% endblock content_footer %}

+ 16 - 14
lib/views/admin/external-accounts.html

@@ -2,7 +2,7 @@
 
 {% block html_title %}外部アカウント管理 · {% endblock %}
 
-{% block content_head %}
+{% block content_header %}
 <div class="header-wrap">
   <header id="page-header">
     <h1 class="title" id="">ユーザー管理/外部アカウント管理</h1>
@@ -41,14 +41,14 @@
     <div class="col-md-9">
       <p>
         <a class="btn btn-default" href="/admin/users">
-          <i class="fa fa-arrow-left" aria-hidden="true"></i>
+          <i class="icon-fw ti-arrow-left" aria-hidden="true"></i>
           ユーザー管理に戻る
         </a>
       </p>
 
       <h2>外部アカウント一覧</h2>
 
-      <table class="table table-hover table-striped table-bordered table-user-list">
+      <table class="table table-bordered table-user-list">
         <thead>
           <tr>
             <th width="120px">Authentication Provider</th>
@@ -62,12 +62,12 @@
                   data-animation="false" data-html="true"
                   data-content="<small>関連付けられているユーザーがパスワードを設定しているかどうかを表示します</small>">
                 <small>
-                  <i class="fa fa-info-circle" aria-hidden="true"></i>
+                  <i class="icon-question" aria-hidden="true"></i>
                 </small>
               </a>
             </th>
-            <th width="100px">作成日</th>
-            <th width="90px">操作</th>
+            <th width="100px">{{ t('user_management.Date created') }}</th>
+            <th width="70px"></th>
           </tr>
         </thead>
         <tbody>
@@ -96,16 +96,18 @@
               <div class="btn-group admin-user-menu">
 
                 <button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown">
-                  編集
-                  <span class="caret"></span>
+                  <i class="icon-settings"></i> <span class="caret"></span>
                 </button>
                 <ul class="dropdown-menu" role="menu">
-                  <li class="dropdown-header">編集メニュー</li>
-                  <li class="dropdown-button">
-                    <form action="/admin/users/external-accounts/{{ account.accountId }}/remove" method="post">
-                      <input type="hidden" name="_csrf" value="{{ csrf() }}">
-                      <button type="submit" class="btn btn-block btn-danger">削除する</button>
-                    </form>
+                  <li class="dropdown-header">{{ t('user_management.Edit menu') }}</li>
+                  <form id="form_remove_{{ account.accountId }}" action="/admin/users/external-accounts/{{ account.accountId }}/remove" method="post">
+                    <input type="hidden" name="_csrf" value="{{ csrf() }}">
+                  </form>
+                  <li>
+                    <a href="javascript:form_remove_{{ account.accountId }}.submit()">
+                      <i class="icon-fw icon-fire text-danger"></i>
+                      削除する
+                    </a>
                   </li>
                 </ul>{# end of .dropdown-menu #}
 

+ 9 - 5
lib/views/admin/index.html

@@ -2,7 +2,7 @@
 
 {% block html_title %}{{ t('admin_top.Management Wiki') }}· {{ path }}{% endblock %}
 
-{% block content_head %}
+{% block content_header %}
 <div class="header-wrap">
   <header id="page-header">
     <h1 class="title" id=""> {{ t('admin_top.Management Wiki') }}</h1>
@@ -29,11 +29,13 @@
       {{ t("admin_top.assign_administrator") }}
       </p>
 
-      <h3>{{ t('admin_top.System Information') }}</h3>
+      <legend>
+        <h2>{{ t('admin_top.System Information') }}</h2>
+      </legend>
       <table class="table table-bordered">
         <tr>
-          <th class="col-sm-4">crowi-plus</th>
-          <td>{{ crowiVersion() }}</td>
+          <th class="col-sm-4">GROWI</th>
+          <td>{{ growiVersion() }}</td>
         </tr>
         <tr>
           <th>node.js</th>
@@ -49,7 +51,9 @@
         </tr>
       </table>
 
-      <h3>{{ t('admin_top.List of installed plugins') }}</h3>
+      <legend>
+        <h2>{{ t('admin_top.List of installed plugins') }}</h2>
+      </legend>
       <table class="table table-bordered">
         <th class="text-center">
           {{ t('admin_top.Package name') }}

+ 9 - 9
lib/views/admin/markdown.html

@@ -3,7 +3,7 @@
 {% block html_title %}{{ t('Markdown settings') }}
  · {{ path }}{% endblock %}
 
-{% block content_head %}
+{% block content_header %}
 <div class="header-wrap">
   <header id="page-header">
     <h1 class="title" id="">{{ t('Markdown settings') }}</h1>
@@ -48,13 +48,13 @@
           </label>
           <div class="col-xs-5">
             <div class="btn-group btn-toggle" data-toggle="buttons">
-              <label class="btn btn-default {% if markdownSetting['markdown:isEnabledLinebreaks'] %}active{% endif %}" data-active-class="primary">
+              <label class="btn btn-default btn-rounded btn-outline {% if markdownSetting['markdown:isEnabledLinebreaks'] %}active{% endif %}" data-active-class="primary">
                 <input name="markdownSetting[markdown:isEnabledLinebreaks]" value="true" type="radio"
-                    {% if true === markdownSetting['markdown:isEnabledLinebreaks'] %}checked{% endif %}> {{ t('valid') }}
+                    {% if true === markdownSetting['markdown:isEnabledLinebreaks'] %}checked{% endif %}> ON
               </label>
-              <label class="btn btn-default {% if !markdownSetting['markdown:isEnabledLinebreaks'] %}active{% endif %}" data-active-class="primary">
+              <label class="btn btn-default btn-rounded btn-outline {% if !markdownSetting['markdown:isEnabledLinebreaks'] %}active{% endif %}" data-active-class="default">
                 <input name="markdownSetting[markdown:isEnabledLinebreaks]" value="false" type="radio"
-                    {% if !markdownSetting['markdown:isEnabledLinebreaks'] %}checked{% endif %}> {{ t('invalid') }}
+                    {% if !markdownSetting['markdown:isEnabledLinebreaks'] %}checked{% endif %}> OFF
               </label>
             </div>
             <p class="help-block">{{ t("markdown_setting.treat_text") }}
@@ -68,13 +68,13 @@
           </label>
           <div class="col-xs-5">
             <div class="btn-group btn-toggle" data-toggle="buttons">
-              <label class="btn btn-default {% if markdownSetting['markdown:isEnabledLinebreaksInComments'] %}active{% endif %}" data-active-class="primary">
+              <label class="btn btn-default btn-rounded btn-outline {% if markdownSetting['markdown:isEnabledLinebreaksInComments'] %}active{% endif %}" data-active-class="primary">
                 <input name="markdownSetting[markdown:isEnabledLinebreaksInComments]" value="true" type="radio"
-                    {% if true === markdownSetting['markdown:isEnabledLinebreaksInComments'] %}checked{% endif %}> {{ t('valid') }}
+                    {% if true === markdownSetting['markdown:isEnabledLinebreaksInComments'] %}checked{% endif %}> ON
               </label>
-              <label class="btn btn-default {% if !markdownSetting['markdown:isEnabledLinebreaksInComments'] %}active{% endif %}" data-active-class="primary">
+              <label class="btn btn-default btn-rounded btn-outline {% if !markdownSetting['markdown:isEnabledLinebreaksInComments'] %}active{% endif %}" data-active-class="default">
                 <input name="markdownSetting[markdown:isEnabledLinebreaksInComments]" value="false" type="radio"
-                    {% if !markdownSetting['markdown:isEnabledLinebreaksInComments'] %}checked{% endif %}> {{ t('invalid') }}
+                    {% if !markdownSetting['markdown:isEnabledLinebreaksInComments'] %}checked{% endif %}> OFF
               </label>
             </div>
             <p class="help-block">{{ t("markdown_setting.treat_comment") }}<br>{{ t("markdown_setting.TBD") }}</p>

+ 22 - 22
lib/views/admin/notification.html

@@ -2,7 +2,7 @@
 
 {% block html_title %}{{ t('Notification settings') }} · {{ path }}{% endblock %}
 
-{% block content_head %}
+{% block content_header %}
 <div class="header-wrap">
   <header id="page-header">
     <h1 class="title" id="">{{ t('Notification settings') }}</h1>
@@ -38,10 +38,10 @@
 
       <ul class="nav nav-tabs" role="tablist">
         <li class="active">
-          <a href="#slack-app" data-toggle="tab" role="tab"><i class="fa fa-slack"></i> Slack App</a>
+          <a href="#slack-app" data-toggle="tab" role="tab"><i class="icon-settings"></i> Slack App</a>
         </li>
         <li role="tab">
-          <a href="#slack-incoming-webhooks" data-toggle="tab" role="tab"><i class="fa fa-slack"></i> Slack Incoming Webhooks</a>
+          <a href="#slack-incoming-webhooks" data-toggle="tab" role="tab"><i class="icon-settings"></i> Slack Incoming Webhooks</a>
         </li>
       </ul>
 
@@ -52,11 +52,11 @@
             <fieldset>
               <legend>Slack App Configuration</legend>
 
-              <p class="well text-warning">
-                <i class="fa fa-warning"></i> NOT RECOMMENDED
+              <p class="well">
+                <i class="icon-fw icon-exclamation text-danger"></i><span class="text-danger">NOT RECOMMENDED</span>
                 <br><br>
-                This is the way that compatible with the official Crowi,<br>
-                but not recommended in crowi-plus because it is too complex.
+                This is the way that compatible with Crowi,<br>
+                but not recommended in GROWI because it is too complex.
                 <br><br>
                 Please use <a href="#slack-incoming-webhooks" data-toggle="tab" onclick="activateTab('slack-incoming-webhooks')">Slack incomming webhooks Configuration</a> instead.
               </p>
@@ -88,16 +88,16 @@
           <div class="text-center">
             {% if hasSlackToken %}
             <p>Crowi and Slack is already <strong>connected</strong>. You can re-connect to refresh and overwirte the token with your Slack account.</p>
-            <a class="btn btn-warning" href="/admin/notification/slackSetting/disconnect">
-              <i class="fa fa-slack"></i> Disconnect from Slack
+            <a class="btn btn-warning btn-rounded" href="/admin/notification/slackSetting/disconnect">
+              <i class="icon-power"></i> Disconnect from Slack
             </a>
-            <a class="btn btn-default" href="{{ slackAuthUrl }}" target="_blank">
-              <i class="fa fa-slack"></i> Reconnect to Slack
+            <a class="btn btn-success btn-outline btn-rounded" href="{{ slackAuthUrl }}" target="_blank">
+              <i class="icon-login"></i> Reconnect to Slack
             </a>
             {% else %}
             <p>Slack clientId and clientSecret is configured. Now, you can connect with Slack.</p>
-            <a class="btn btn-primary" href="{{ slackAuthUrl }}" target="_blank">
-              <i class="fa fa-slack"></i> Connect to Slack
+            <a class="btn btn-primary btn-outline2 btn-rounded" href="{{ slackAuthUrl }}" target="_blank">
+              <i class="icon-login"></i> Connect to Slack
             </a>
             {% endif %}
           </div>
@@ -107,7 +107,7 @@
           {# {% if not hasSlackWebClientConfig %} #}
           <hr>
           <h3>
-            <i class="fa fa-question-circle" aria-hidden="true"></i>
+            <i class="icon-question" aria-hidden="true"></i>
             <a href="#collapseHelpForApp" data-toggle="collapse">How to configure Slack App?</a>
           </h3>
 
@@ -118,7 +118,7 @@
                 <li>
                   Create App from <a href="https://api.slack.com/applications/new">this link</a>, and fill the form out as below:
                   <dl class="dl-horizontal">
-                    <dt>App Name</dt> <dd><code>crowi-plus</code> </dd>
+                    <dt>App Name</dt> <dd><code>growi</code> </dd>
                     <dt>Development Slack Workspace</dt> <dd>Select the workspace you want to notify to.</dd>
                   </dl>
                 </li>
@@ -143,7 +143,7 @@
               Set Permission Scopes to the App
               <ol>
                 <li>Go to "OAuth &amp; Permissions" page.</li>
-                <li>Add "Send messages as crowi-plus"(<code>chat:write:bot</code>).</li>
+                <li>Add "Send messages as GROWI"(<code>chat:write:bot</code>).</li>
                 <li>Don't forget to <strong>save</strong>.</li>
               </ol>
             </li>
@@ -162,7 +162,7 @@
             <li>
               (At Workspace) Approve the app
               <ol>
-                <li>Go to the management Apps page for the workspace you installed the app and approve crowi-plus.</li>
+                <li>Go to the management Apps page for the workspace you installed the app and approve "growi".</li>
               </ol>
             </li>
             <li>
@@ -172,10 +172,10 @@
               </ol>
             </li>
             <li>
-              (At crowi-plus) Input "clientId" and "clientSecret" and submit on this page.
+              (At GROWI admin page) Input "clientId" and "clientSecret" and submit on this page.
             </li>
             <li>
-              (At crowi-plus) Click "Connect to Slack" button to start OAuth process.
+              (At GROWI admin page) Click "Connect to Slack" button to start OAuth process.
             </li>
           </ol>
           {# {% endif %} #}
@@ -202,7 +202,7 @@
                   <input type="checkbox" name="slackIwhSetting[slack:isIncomingWebhookPrioritized]" value="1"
                     {% if slackSetting['slack:isIncomingWebhookPrioritized'] %}checked{% endif %}>
                   Prioritize Incoming Webhook than Slack App
-                  <p class="help-block">Check this option and crowi-plus use Incoming Webhooks even if Slack App settings are enabled.</p>
+                  <p class="help-block">Check this option and GROWI use Incoming Webhooks even if Slack App settings are enabled.</p>
                 </div>
               </div>
 
@@ -217,7 +217,7 @@
 
           <hr>
           <h3>
-            <i class="fa fa-question-circle" aria-hidden="true"></i>
+            <i class="icon-question" aria-hidden="true"></i>
             <a href="#collapseHelpForIwh" data-toggle="collapse">How to configure Incoming Webhooks?</a>
           </h3>
 
@@ -231,7 +231,7 @@
               </ol>
             </li>
             <li>
-              (At crowi-plus) Set Webhook URL
+              (At GROWI admin page) Set Webhook URL
               <ol>
                 <li>Input "Webhook URL" and submit on this page.</li>
               </ol>

+ 2 - 2
lib/views/admin/search.html

@@ -2,7 +2,7 @@
 
 {% block html_title %}検索管理 · {{ path }}{% endblock %}
 
-{% block content_head %}
+{% block content_header %}
 <div class="header-wrap">
   <header id="page-header">
     <h1 class="title" id="">検索管理</h1>
@@ -42,7 +42,7 @@
         <div class="form-group">
           <label for="" class="col-xs-3 control-label">Index Build</label>
           <div class="col-xs-6">
-            <button type="submit" class="btn btn-primary">Build Now</button>
+            <button type="submit" class="btn btn-inverse">Build Now</button>
             <p class="help-block">
               Force rebuild index.<br>
               Click "Build Now" to delete and create mapping file and add all pages.<br>

+ 18 - 18
lib/views/admin/security.html

@@ -2,7 +2,7 @@
 
 {% block html_title %}{{ t('Security settings') }} · {% endblock %}
 
-{% block content_head %}
+{% block content_header %}
 <div class="header-wrap">
   <header id="page-header">
     <h1 class="title" id="">{{ t('Security settings') }}</h1>
@@ -38,7 +38,7 @@
 
       <form action="/_api/admin/security/general" method="post" class="form-horizontal" id="generalSetting" role="form">
         <fieldset>
-        <legend>{{ t('security_setting.Security settings') }}</legend>
+        <legend class="alert-anchor">{{ t('security_setting.Security settings') }}</legend>
 
           <div class="form-group">
             <label for="settingForm[security:registrationMode]" class="col-xs-3 control-label">{{ t('Basic authentication') }}</label>
@@ -61,7 +61,7 @@
           <div class="form-group">
             <label for="settingForm[security:restrictGuestMode]" class="col-xs-3 control-label">{{ t('Guest users access') }}</label>
             <div class="col-xs-6">
-              <select class="form-control" name="settingForm[security:restrictGuestMode]" value="{{ settingForm['security:restrictGuestMode'] }}">
+              <select class="form-control selectpicker" name="settingForm[security:restrictGuestMode]" value="{{ settingForm['security:restrictGuestMode'] }}">
                 {% for modeValue, modeLabel in consts.restrictGuestMode %}
                 <option value="{{ modeValue }}" {% if modeValue == settingForm['security:restrictGuestMode'] %}selected{% endif %} >{{ modeLabel }}</option>
                 {% endfor %}
@@ -72,7 +72,7 @@
           <div class="form-group">
             <label for="settingForm[security:registrationMode]" class="col-xs-3 control-label">{{ t('Register limitation') }}</label>
             <div class="col-xs-6">
-              <select class="form-control" name="settingForm[security:registrationMode]" value="{{ settingForm['security:registrationMode'] }}">
+              <select class="form-control selectpicker" name="settingForm[security:registrationMode]" value="{{ settingForm['security:registrationMode'] }}">
                 {% for modeValue, modeLabel in consts.registrationMode %}
                 <option value="{{ modeValue }}" {% if modeValue == settingForm['security:registrationMode'] %}selected{% endif %} >{{ modeLabel }}</option>
                 {% endfor %}
@@ -100,9 +100,9 @@
         </fieldset>
       </form>
 
-      <form action="/_api/admin/security/mechanism" method="post" class="form-horizontal" id="mechanismSetting" role="form">
+      <form action="/_api/admin/security/mechanism" method="post" class="form-horizontal m-t-30" id="mechanismSetting" role="form">
         <fieldset>
-          <legend>{{ t('Selecting authentication mechanism') }}</legend>
+          <legend class="alert-anchor">{{ t('Selecting authentication mechanism') }}</legend>
           <p class="alert alert-info"><b>NOTE: </b>Restarting the server is needed if you switch the auth mechanism.</p>
           <div class="form-group">
             <div class="col-xs-6">
@@ -145,7 +145,7 @@
       </form>
 
 
-      <div class="auth-mechanism-configurations">
+      <div class="auth-mechanism-configurations m-t-10">
 
         <legend>{{ t('security_setting.Authentication mechanism settings') }}</legend>
 
@@ -155,18 +155,18 @@
           <p class="alert alert-warning"
               {% if !isRestartingServerNeeded %}style="display: none;"{% endif %}>
             <b>
-              <i class="fa fa-exclamation-circle" aria-hidden="true"></i>
+              <i class="icon-exclamation" aria-hidden="true"></i>
               Restarting the server is needed.
             </b>
             The server is running with Passport authentication mechanism.
           </p>
 
-          <form action="/_api/admin/security/google" method="post" class="form-horizontal " id="googleSetting" role="form"
+          <form action="/_api/admin/security/google" method="post" class="form-horizontal" id="googleSetting" role="form"
               {% if isRestartingServerNeeded %}style="opacity: 0.4;"{% endif %}>
 
             <fieldset>
               <h4>Google 設定</h4>
-              <p class="well">
+              <p class="well alert-anchor">
                 Google Cloud Platform の <a href="https://console.cloud.google.com/apis/credentials">API Manager</a>
                 から OAuth2 Client ID を作成すると、Google アカウントにコネクトして登録やログインが可能になります。
               </p>
@@ -217,30 +217,30 @@
           <p class="alert alert-warning"
               {% if !isRestartingServerNeeded %}style="display: none;"{% endif %}>
             <b>
-              <i class="fa fa-exclamation-circle" aria-hidden="true"></i>
+              <i class="icon-exclamation" aria-hidden="true"></i>
               Restarting the server is needed.
             </b>
             The server is running with Official Crowi authentication mechanism.
           </p>
           <ul class="nav nav-tabs" role="tablist" {% if isRestartingServerNeeded %}style="opacity: 0.4;"{% endif %}>
             <li class="active">
-              <a href="#passport-ldap" data-toggle="tab" role="tab"><i class="fa fa-sitemap"></i> LDAP</a>
+              <a href="#passport-ldap" data-toggle="tab" role="tab"><i class="icon-organization"></i> LDAP</a>
             </li>
             <li>
-              <a href="#passport-google-oauth" data-toggle="tab" role="tab"><i class="fa fa-google"></i> Google OAuth</a>
+              <a href="#passport-google-oauth" data-toggle="tab" role="tab"><i class="icon-social-google"></i> Google OAuth</a>
             </li>
             <li>
-              <a href="#passport-facebook" data-toggle="tab" role="tab"><i class="fa fa-facebook"></i> Facebook</a>
+              <a href="#passport-facebook" data-toggle="tab" role="tab"><i class="icon-social-facebook"></i> Facebook</a>
             </li>
             <li>
-              <a href="#passport-twitter" data-toggle="tab" role="tab"><i class="fa fa-twitter"></i> Twitter</a>
+              <a href="#passport-twitter" data-toggle="tab" role="tab"><i class="icon-social-twitter"></i> Twitter</a>
             </li>
             <li>
-              <a href="#passport-github" data-toggle="tab" role="tab"><i class="fa fa-github"></i> Github</a>
+              <a href="#passport-github" data-toggle="tab" role="tab"><i class="icon-social-github"></i> Github</a>
             </li>
           </ul>
 
-          <div class="tab-content" {% if isRestartingServerNeeded %}style="opacity: 0.4;"{% endif %}>
+          <div class="tab-content p-t-10" {% if isRestartingServerNeeded %}style="opacity: 0.4;"{% endif %}>
             <div id="passport-ldap" class="tab-pane active" role="tabpanel" >
               {% include './widget/passport/ldap.html' with { settingForm: settingForm } %}
             </div>
@@ -282,7 +282,7 @@
           var $message = $('<p class="alert"></p>');
           $message.addClass('alert-' + status);
           $message.html(msg.replace('\n', '<br>'));
-          $message.insertAfter('#' + formId + ' legend');
+          $message.insertAfter('#' + formId + ' .alert-anchor');
 
           if (status == 'success') {
             setTimeout(function()

+ 248 - 0
lib/views/admin/user-group-detail.html

@@ -0,0 +1,248 @@
+{% extends '../layout/admin.html' %}
+
+{% block html_title %}グループ管理 · {% endblock %}
+
+{% block content_header %}
+<div class="header-wrap">
+  <header id="page-header">
+    <h1 class="title" id="">グループ管理(グループ詳細)</h1>
+  </header>
+</div>
+{% endblock %}
+
+{% block content_main %}
+<div class="content-main">
+  {% set smessage = req.flash('successMessage') %}
+  {% if smessage.length %}
+  <div class="alert alert-success">
+    {{ smessage }}
+  </div>
+  {% endif %}
+
+  {% set emessage = req.flash('errorMessage') %}
+  {% if emessage.length %}
+  <div class="alert alert-danger">
+    {{ emessage }}
+  </div>
+  {% endif %}
+
+  <div class="row">
+    <div class="col-md-3">
+      {% include './widget/menu.html' with {current: 'user-group'} %}
+    </div>
+
+    <div class="col-md-9">
+      <a href="/admin/user-groups" class="btn btn-default">
+        <i class="icon-fw ti-arrow-left" aria-hidden="true"></i>
+        グループ一覧に戻る
+      </a>
+
+      <div class="modal fade" id="admin-add-user-group-relation-modal">
+        <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>
+              <h4 class="modal-title">
+                グループへのユーザー追加
+              </h4>
+            </div>
+
+            <div class="modal-body">
+              <p>
+                <strong>方法1.</strong> ユーザ名を入力して追加
+              </p>
+              <form class="form-inline" role="form" action="/admin/user-group-relation/create" method="post">
+                <div class="form-group">
+                  <input type="text" name="user_name" class="form-control input-sm" id="inputRelatedUserName" placeholder="username">
+                </div>
+                <input type="hidden" name="user_group_id" value="{{userGroup.id}}">
+                <input type="hidden" name="_csrf" value="{{ csrf() }}">
+                <button type="submit" class="btn btn-sm btn-success">追加</button>
+              </form>
+
+              {% if 0 < notRelatedusers.length %}
+              <hr>
+              <p>
+                <strong>方法2.</strong> ユーザーを下のリストから選択
+              </p>
+
+              <ul class="list-inline">
+                {% for sUser in notRelatedusers %}
+                <li>
+                  <form role="form" action="/admin/user-group-relation/create" method="post">
+                    <!-- <input type="hidden" name="user_name" value="{{sUser.username}}"> -->
+                    <input type="hidden" name="user_group_id" value="{{userGroup.id}}">
+                    <input type="hidden" name="_csrf" value="{{ csrf() }}">
+                    <button type="submit" name="user_name" value="{{sUser.username}}" class="btn btn-xs btn-primary">{{sUser.username}}</button>
+                  </form>
+                </li>
+                {% endfor %}
+              </ul>
+              {% endif %}
+
+            </div>
+
+          </div>
+          <!-- /.modal-content -->
+        </div>
+        <!-- /.modal-dialog -->
+      </div>
+
+      <div class="m-t-20 form-box">
+        <form action="/admin/user-group/{{userGroup.id}}/update" method="post" class="form-horizontal" role="form">
+          <fieldset>
+            <legend>基本情報</legend>
+            <div class="form-group">
+              <label for="name" class="col-sm-2 control-label">{{ t('Name') }}</label>
+              <div class="col-sm-4">
+                <input class="form-control" type="text" name="name" value="{{ userGroup.name }}" required>
+              </div>
+            </div>
+            <div class="form-group">
+              <label class="col-sm-2 control-label">{{ t('Created') }}</label>
+              <div class="col-sm-4">
+                <input class="form-control" type="text" disabled value="{{userGroup.createdAt|date('Y-m-d', sRelation.relatedUser.createdAt.getTimezoneOffset()) }}">
+              </div>
+            </div>
+            <div class="form-group">
+              <div class="col-sm-offset-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="m-t-20 form-box">
+        <fieldset>
+          <legend>グループ画像の設定</legend>
+          <div class="form-group col-sm-8">
+            <h4>
+              {{ t('Upload Image') }}
+            </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="{{ userGroup|uploadedpicture }}" id="settingUserPicture" class="picture picture-lg img-circle">
+                  <br>
+                </p>
+                <p>
+                  {% if userGroup.image %}
+                  <form action="/admin/user-group/{{userGroup.id}}/picture/delete" method="post" class="form-horizontal" role="form" onsubmit="return window.confirm('{{ t('Delete this image?') }}');">
+                    <button type="submit" class="btn btn-danger">{{ t('Delete Image') }}</button>
+                  </form>
+                  {% endif %}
+                </p>
+              </div>
+            </div><!-- /.form-group -->
+
+            <div class="form-group">
+              <label for="" class="col-sm-4 control-label">
+                {{ t('Upload new image') }}
+              </label>
+              <div class="col-sm-8">
+                {% if isUploadable() %}
+                <form action="/_api/admin/user-group/{{userGroup.id}}/picture/upload" id="pictureUploadForm" method="post" class="form-horizontal" role="form" enctype="multipart/form-data">
+                  <input name="userGroupPicture" type="file" accept="image/*">
+                  <div id="pictureUploadFormProgress">
+                  </div>
+                </form>
+                {% else %} * {{ t('page_me.form_help.profile_image1') }}
+                <br> * {{ t('page_me.form_help.profile_image2') }}
+                <br> {% endif %}
+              </div>
+            </div><!-- /.form-group -->
+
+          </div><!-- /.col-sm- -->
+
+        </fieldset>
+      </div><!-- /.form-box -->
+
+      <legend class="m-t-20">ユーザー一覧</legend>
+
+      <table class="table table-bordered table-user-list">
+        <thead>
+          <tr>
+            <th width="100px">#</th>
+            <th>
+              <code>username</code>
+            </th>
+            <th>名前</th>
+            <th width="100px">作成日</th>
+            <th width="150px">最終ログイン</th>
+            <th width="70px"></th>
+          </tr>
+        </thead>
+        <tbody>
+          {% for sRelation in userGroupRelations %}
+          {% set sUser = sRelation.relatedUser%}
+          <tr>
+            <td>
+              <img src="{{ sRelation.relatedUser|picture }}" class="picture img-circle" />
+            </td>
+            <td>
+              <strong>{{ sRelation.relatedUser.username }}</strong>
+            </td>
+            <td>{{ sRelation.relatedUser.name }}</td>
+            <td>{{ sRelation.relatedUser.createdAt|date('Y-m-d', sRelation.relatedUser.createdAt.getTimezoneOffset()) }}</td>
+            <td>
+              {% if sRelation.relatedUser.lastLoginAt %} {{ sRelation.relatedUser.lastLoginAt|date('Y-m-d H:i', sRelation.relatedUser.createdAt.getTimezoneOffset()) }} {% endif %}
+            </td>
+            <td>
+              <div class="btn-group admin-user-menu">
+                <button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown">
+                  <i class="icon-settings"></i> <span class="caret"></span>
+                </button>
+                <ul class="dropdown-menu" role="menu">
+                  <form id="form_removeFromGroup_{{ sUser.id }}" action="/admin/user-group-relation/{{userGroup.name}}/remove-relation/{{ sRelation._id.toString() }}" method="post">
+                    <input type="hidden" name="_csrf" value="{{ csrf() }}">
+                  </form>
+                  <li>
+                    <a href="javascript:form_removeFromGroup_{{ sUser.id }}.submit()">
+                      <i class="icon-fw icon-user-unfollow"></i> グループから外す
+                    </a>
+                  </li>
+                </ul>
+              </div>
+            </td>
+          </tr>
+          {% endfor %}
+
+          {% if 0 < notRelatedusers.length %}
+          <tr>
+            <td></td>
+            <td class="text-center">
+              <button class="btn btn-default" data-target="#admin-add-user-group-relation-modal" data-toggle="modal">
+                <i class="ti-plus"></i>
+              </button>
+            </td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+          </tr>
+          {% endif %}
+        </tbody>
+      </table>
+
+      <!-- {% include '../widget/pager.html' with {path: "/admin/user-group-detail", pager: pager} %} -->
+
+      <legend class="m-t-20">ページ一覧</legend>
+
+      {% if pageGroupRelations.length == 0 %}<p>グループが閲覧権限を保有するページはありません</p>{% endif %}
+      {% include '../widget/page_list.html' with { pages: pageGroupRelations, pagePropertyName: 'targetPage' } %}
+
+    </div>
+  </div>
+</div>
+{% endblock content_main %}
+
+{% block content_footer %}
+{% endblock content_footer %}
+
+

+ 173 - 0
lib/views/admin/user-groups.html

@@ -0,0 +1,173 @@
+{% extends '../layout/admin.html' %}
+
+{% block html_title %}グループ管理 · {% endblock %}
+
+{% block content_header %}
+<div class="header-wrap">
+  <header id="page-header">
+    <h1 class="title" id="">グループ管理</h1>
+  </header>
+</div>
+{% endblock %}
+
+{% block content_main %}
+<div class="content-main">
+  {% set smessage = req.flash('successMessage') %}
+  {% if smessage.length %}
+  <div class="alert alert-success">
+    {{ smessage }}
+  </div>
+  {% endif %}
+
+  {% set emessage = req.flash('errorMessage') %}
+  {% if emessage.length %}
+  <div class="alert alert-danger">
+    {{ emessage }}
+  </div>
+  {% endif %}
+
+  <div class="row">
+    <div class="col-md-3">
+      {% include './widget/menu.html' with {current: 'user-group'} %}
+    </div>
+
+    <div class="col-md-9">
+      <p>
+        <button  data-toggle="collapse" class="btn btn-default" href="#createGroupForm">新規グループの作成</button>
+      </p>
+      <form role="form" action="/admin/user-group/create" method="post">
+        <div id="createGroupForm" class="collapse">
+          <div class="form-group">
+            <label for="createGroupForm[userGroupName]">グループ名</label>
+            <textarea class="form-control" name="createGroupForm[userGroupName]" placeholder="例: Group1"></textarea>
+          </div>
+          <button type="submit" class="btn btn-primary">作成する</button>
+        </div>
+        <input type="hidden" name="_csrf" value="{{ csrf() }}">
+      </form>
+
+      {% set createdUserGroup = req.flash('createdUserGroup') %}
+      {% if createdUserGroup.length %}
+      <div class="modal fade in" id="createdGroupModal">
+        <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>
+              <h4 class="modal-title">グループを作成しました</h4>
+            </div>
+
+            <div class="modal-body">
+              <p>
+                作成したグループにユーザを追加してください
+              </p>
+
+              <pre>{{ createdUserGroup.name }}</pre>
+            </div>
+
+          </div><!-- /.modal-content -->
+        </div><!-- /.modal-dialog -->
+      </div><!-- /.modal -->
+      {% endif %}
+
+      <div class="modal fade" id="admin-delete-user-group-modal">
+        <div class="modal-dialog">
+          <div class="modal-content">
+            <div class="modal-header bg-danger">
+              <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
+              <div class="modal-title">
+                <i class="icon icon-fire"></i> グループの削除
+              </div>
+            </div>
+
+            <div class="modal-body">
+              <dl>
+                <dt>グループ名</dt>
+                <dd><span id="admin-delete-user-group-name"></span></dd>
+              </dl>
+              <span class="text-danger">
+                グループの削除を行うと元に戻すことはできませんのでご注意ください。
+              </span>
+            </div>
+            <div class="modal-footer">
+              <form action="/admin/user-group.remove" method="post" id="admin-user-groups-delete" class="text-right">
+                <input type="hidden" name="user_group_id" value="">
+                <input type="hidden" name="_csrf" value="{{ csrf() }}">
+                <button type="submit" value="" class="btn btn-sm btn-danger">
+                  <i class="icon icon-fire"></i> 削除
+                </button>
+              </form>
+            </div>
+
+          </div>
+          <!-- /.modal-content -->
+        </div>
+        <!-- /.modal-dialog -->
+      </div>
+
+      <h2>グループ一覧</h2>
+
+      <table class="table table-bordered table-user-list">
+        <thead>
+          <tr>
+            <th width="100px">#</th>
+            <th>{{ t('Name') }}</th>
+            <th>ユーザ一覧</th>
+            <th width="100px">作成日</th>
+            <th width="70px"></th>
+          </tr>
+        </thead>
+        <tbody>
+          {% for sGroup in userGroups %}
+          <tr>
+            <td>
+              <img src="{{ sGroup|picture }}" class="picture img-circle" />
+            </td>
+            <td>{{ sGroup.name }}</td>
+            <td><ul class="list-inline">
+              {% for relation in userGroupRelations.get(sGroup) %}
+              <li class="list-inline-item badge badge-primary">{{relation.relatedUser.username}}</li>
+              {% endfor %}
+            </ul></td>
+            <td>{{ sGroup.createdAt|date('Y-m-d', sGroup.createdAt.getTimezoneOffset()) }}</td>
+            <td>
+              <div class="btn-group admin-group-menu">
+                <button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown">
+                  <i class="icon-settings"></i> <span class="caret"></span>
+                </button>
+                <ul class="dropdown-menu" role="menu">
+                  <li>
+                    <a href="/admin/user-group-detail/{{sGroup.name}}">
+                      <i class="icon-fw icon-note"></i> 編集
+                    </a>
+                  </li>
+
+                  <li>
+                    <a href="#"
+                        data-user-group-id="{{ sGroup._id.toString() }}"
+                        data-user-group-name="{{ sGroup.name.toString() }}"
+                        data-target="#admin-delete-user-group-modal"
+                        data-toggle="modal">
+                      <i class="icon-fw icon-fire text-danger"></i> 削除する
+                    </a>
+                  </li>
+
+                </ul>
+              </div>
+            </td>
+          </tr>
+          {% endfor %}
+        </tbody>
+      </table>
+
+      {% include '../widget/pager.html' with {path: "/admin/user-groups", pager: pager} %}
+
+    </div>
+  </div>
+</div>
+{% endblock content_main %}
+
+{% block content_footer %}
+{% endblock content_footer %}
+
+

+ 85 - 63
lib/views/admin/users.html

@@ -2,7 +2,7 @@
 
 {% block html_title %}{{ t('user_management.User management') }}· {% endblock %}
 
-{% block content_head %}
+{% block content_header %}
 <div class="header-wrap">
   <header id="page-header">
     <h1 class="title" id="">{{ t('user_management.User management') }}</h1>
@@ -33,9 +33,11 @@
 
     <div class="col-md-9">
       <p>
-        <button data-toggle="collapse" class="btn btn-default" href="#inviteUserForm">{{ t("user_management.invite_users") }}</button>
-        <a class="btn btn-default" href="/admin/users/external-accounts">
-          <i class="fa fa-user-plus" aria-hidden="true"></i>
+        <button data-toggle="collapse" class="btn btn-default" href="#inviteUserForm">
+          {{ t("user_management.invite_users") }}
+        </button>
+        <a class="btn btn-default btn-outline" href="/admin/users/external-accounts">
+          <i class="icon-user-follow" aria-hidden="true"></i>
           {{ t("user_management.external_account") }}
         </a>
       </p>
@@ -63,7 +65,7 @@
 
             <div class="modal-header">
               <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
-              <h4 class="modal-title">ユーザーを招待しました</h4>
+              <div class="modal-title">ユーザーを招待しました</div>
             </div>
 
             <div class="modal-body">
@@ -86,7 +88,7 @@
           <div class="modal-content">
             <div class="modal-header">
               <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
-              <h4 class="modal-title">パスワードを新規発行しますか?</h4>
+              <div class="modal-title">パスワードを新規発行しますか?</div>
             </div>
 
             <div class="modal-body">
@@ -117,7 +119,7 @@
 
             <div class="modal-header">
               <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
-              <h4 class="modal-title">Password reset!</h4>
+              <div class="modal-title">Password reset!</div>
             </div>
 
             <div class="modal-body">
@@ -138,7 +140,7 @@
 
       <h2>{{ t("user_management.user_list") }}</h2>
 
-      <table class="table table-hover table-striped table-bordered table-user-list">
+      <table class="table table-default table-bordered table-user-list">
         <thead>
           <tr>
             <th width="100px">#</th>
@@ -148,16 +150,17 @@
             <th>{{ t('Email') }}</th>
             <th width="100px">{{ t('user_management.Date created') }}</th>
             <th width="150px">{{ t('user_management.Last login') }}</th>
-            <th width="90px">{{ t('user_management.Manage') }}</th>
+            <th width="70px"></th>
           </tr>
         </thead>
         <tbody>
           {% for sUser in users %}
+          {% set sUserId = sUser._id.toString() %}
           <tr>
             <td>
-              <img src="{{ sUser|picture }}" class="picture picture-rounded" />
+              <img src="{{ sUser|picture }}" class="picture img-circle" />
               {% if sUser.admin %}
-              <span class="label label-primary label-admin">
+              <span class="label label-inverse label-admin">
                 Admin
               </span>
               {% endif %}
@@ -180,22 +183,18 @@
             </td>
             <td>
               <div class="btn-group admin-user-menu">
-                <button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown">
-                  {{ t('Edit') }}
-                  <span class="caret"></span>
+                <button type="button" class="btn btn-sm btn-default dropdown-toggle" data-toggle="dropdown">
+                  <i class="icon-settings"></i> <span class="caret"></span>
                 </button>
                 <ul class="dropdown-menu" role="menu">
                   <li class="dropdown-header">{{ t('user_management.Edit menu') }}</li>
                   <li>
-                    <a href="">{{ t('Edit') }}</a>
-
-                  </li>
-                  <li class="dropdown-button">
                     <a href="#"
-                      data-user-id="{{ sUser._id.toString() }}"
-                      data-user-email="{{ sUser.email }}"
-                      data-target="#admin-password-reset-modal"
-                      data-toggle="modal" class="btn btn-block btn-default">
+                        data-user-id="{{ sUserId }}"
+                        data-user-email="{{ sUser.email }}"
+                        data-target="#admin-password-reset-modal"
+                        data-toggle="modal">
+                      <i class="icon-fw icon-key"></i>
                       {{ t('user_management.Reissue password') }}
                     </a>
                   </li>
@@ -203,52 +202,65 @@
                   <li class="dropdown-header">{{ t('user_management.Status') }}</li>
 
                   {% if sUser.status == 1 %}
-                  <li class="dropdown-button">
-                    <form action="/admin/user/{{ sUser._id.toString() }}/activate" method="post">
-                      <input type="hidden" name="_csrf" value="{{ csrf() }}">
-                      <button type="submit" class="btn btn-block btn-info">承認する</button>
-                    </form>
+                  <form id="form_activate_{{ sUserId }}" action="/admin/user/{{ sUserId }}/activate" method="post">
+                    <input type="hidden" name="_csrf" value="{{ csrf() }}">
+                  </form>
+                  <li>
+                    <a href="javascript:form_activate_{{ sUserId }}.submit()">
+                      <i class="icon-fw icon-user-following"></i> 承認する
+                    </a>
                   </li>
                   {% endif  %}
 
                   {% if sUser.status == 2 %}
-                  <li class="dropdown-button">
+                  <form id="form_suspend_{{ sUserId }}" action="/admin/user/{{ sUserId }}/suspend" method="post">
+                    <input type="hidden" name="_csrf" value="{{ csrf() }}">
+                  </form>
+                  <li>
                     {% if sUser.username != user.username %}
-                    <form action="/admin/user/{{ sUser._id.toString() }}/suspend" method="post">
-                      <input type="hidden" name="_csrf" value="{{ csrf() }}">
-                      <button type="submit" class="btn btn-block btn-warning">{{ t('user_management.Deactivate account') }}</button>
-                    </form>
+                    <a href="javascript:form_suspend_{{ sUserId }}.submit()">
+                      <i class="icon-fw icon-ban"></i>
+                      {{ t('user_management.Deactivate account') }}
+                    </a>
                     {% else %}
-                    <button class="btn btn-block btn-warning" disabled>{{ t('user_management.Deactivate account') }}</button>
-                    <br>
-                    <p class="alert alert-danger">{{ t("user_management.your_own") }}</p>
+                    <a disabled>
+                      <i class="icon-fw icon-ban"></i>
+                      {{ t('user_management.Deactivate account') }}
+                    </a>
+                    <p class="alert alert-danger m-l-10 m-r-10 p-10">{{ t("user_management.your_own") }}</p>
                     {% endif %}
                   </li>
                   {% endif %}
 
                   {% if sUser.status == 3 %}
-                  <li class="dropdown-button">
-                    <form action="/admin/user/{{ sUser._id.toString() }}/activate" method="post">
-                      <input type="hidden" name="_csrf" value="{{ csrf() }}">
-                      <button type="submit" class="btn btn-block btn-default">元に戻す</button>
-                    </form>
+                  <form id="form_activate_{{ sUserId }}" action="/admin/user/{{ sUserId }}/activate" method="post">
+                    <input type="hidden" name="_csrf" value="{{ csrf() }}">
+                  </form>
+                  <form id="form_remove_{{ sUserId }}" action="/admin/user/{{ sUserId }}/remove" method="post">
+                    <input type="hidden" name="_csrf" value="{{ csrf() }}">
+                  </form>
+                  <li>
+                    <a href="javascript:form_activate_{{ sUserId }}.submit()">
+                      <i class="icon-fw icon-action-redo"></i> 元に戻す
+                    </a>
                   </li>
-                  <li class="dropdown-button">
+                  <li>
                     {# label は同じだけど、こっちは論理削除 #}
-                    <form action="/admin/user/{{ sUser._id.toString() }}/remove" method="post">
-                      <input type="hidden" name="_csrf" value="{{ csrf() }}">
-                      <button type="submit" class="btn btn-block btn-danger">削除する</button>
-                    </form>
+                    <a href="javascript:form_remove_{{ sUserId }}.submit()">
+                      <i class="icon-fw icon-fire text-danger"></i> 削除する
+                    </a>
                   </li>
                   {% endif  %}
 
                   {% if sUser.status == 1 || sUser.status == 5 %}
+                  <form id="form_removeCompletely_{{ sUserId }}" action="/admin/user/{{ sUser._id.toString() }}/removeCompletely" method="post">
+                    <input type="hidden" name="_csrf" value="{{ csrf() }}">
+                  </form>
                   <li class="dropdown-button">
                     {# label は同じだけど、こっちは物理削除 #}
-                    <form action="/admin/user/{{ sUser._id.toString() }}/removeCompletely" method="post">
-                      <input type="hidden" name="_csrf" value="{{ csrf() }}">
-                      <button type="submit" class="btn btn-block btn-danger">削除する</button>
-                    </form>
+                    <a href="javascript:form_removeCompletely_{{ sUserId }}.submit()">
+                      <i class="icon-fw icon-fire text-danger"></i> 削除する
+                    </a>
                   </li>
                   {% endif  %}
 
@@ -256,23 +268,33 @@
                   <li class="divider"></li>
                   <li class="dropdown-header">{{ t('user_management.Administrator menu') }}</li>
 
-                  <li class="dropdown-button">
-                    {% if sUser.admin %}
-                      {% if sUser.username != user.username %}
-                      <form action="/admin/user/{{ sUser._id.toString() }}/removeFromAdmin" method="post">
-                        <input type="hidden" name="_csrf" value="{{ csrf() }}">
-                        <button type="submit" class="btn btn-block btn-danger">管理者からはずす</button>
-                      </form>
-                      {% else %}
-                      <p class="alert alert-danger">{{ t("user_management.cannot_remove") }}</p>
-                      {% endif %}
+                  {% if sUser.admin %}
+                  <form id="form_removeFromAdmin_{{ sUserId }}" action="/admin/user/{{ sUser._id.toString() }}/removeFromAdmin" method="post">
+                    <input type="hidden" name="_csrf" value="{{ csrf() }}">
+                  </form>
+                  <li>
+                    {% if sUser.username != user.username %}
+                      <a href="javascript:form_removeFromAdmin_{{ sUserId }}.submit()">
+                        <i class="icon-fw icon-user-unfollow"></i> 管理者からはずす
+                      </a>
                     {% else %}
-                      <form action="/admin/user/{{ sUser._id.toString() }}/makeAdmin" method="post">
-                        <input type="hidden" name="_csrf" value="{{ csrf() }}">
-                        <button type="submit" class="btn btn-block btn-primary">管理者にする</button>
-                      </form>
+                      <a disabled>
+                        <i class="icon-fw icon-user-unfollow"></i> 管理者からはずす
+                      </a>
+                      <p class="alert alert-danger m-l-10 m-r-10 p-10">{{ t("user_management.cannot_remove") }}</p>
                     {% endif %}
                   </li>
+                  {% else %}
+                  <form id="form_makeAdmin_{{ sUserId }}" action="/admin/user/{{ sUser._id.toString() }}/makeAdmin" method="post">
+                    <input type="hidden" name="_csrf" value="{{ csrf() }}">
+                  </form>
+                  <li>
+                    <a href="javascript:form_makeAdmin_{{ sUserId }}.submit()">
+                      <i class="icon-fw icon-magic-wand"></i> 管理者にする
+                    </a>
+                  </li>
+                  {% endif %}
+
                   {% endif %}
                 </ul>
               </div>

+ 9 - 8
lib/views/admin/widget/menu.html

@@ -2,14 +2,15 @@
   {% set current = 'index' %}
 {% endif  %}
 <ul class="nav nav-pills nav-stacked">
-  <li class="{% if current == 'index'%}active{% endif %}"><a href="/admin"><i class="fa fa-cube"></i> {{ t('Management Wiki Home') }}</a></li>
-  <li class="{% if current == 'app'%}active{% endif %}"><a href="/admin/app"><i class="fa fa-gears"></i>{{ t('App settings') }}</a></li>
-  <li class="{% if current == 'security'%}active{% endif %}"><a href="/admin/security"><i class="fa fa-shield"></i> {{ t('Security settings') }}</a></li>
-  <li class="{% if current == 'markdown'%}active{% endif %}"><a href="/admin/markdown"><i class="fa fa-pencil"></i> {{ t('Markdown settings') }}</a></li>
-  <li class="{% if current == 'customize'%}active{% endif %}"><a href="/admin/customize"><i class="fa fa-object-group"></i> {{ t('Customize') }}</a></li>
-  <li class="{% if current == 'notification'%}active{% endif %}"><a href="/admin/notification"><i class="fa fa-bell"></i> {{ t('Notification settings') }}</a></li>
-  <li class="{% if current == 'user' || current == 'external-account' %}active{% endif %}"><a href="/admin/users"><i class="fa fa-users"></i> {{ t('User management') }}</a></li>
+  <li class="{% if current == 'index'%}active{% endif %}"><a href="/admin"><i class="icon-fw icon-home"></i> {{ t('Management Wiki Home') }}</a></li>
+  <li class="{% if current == 'app'%}active{% endif %}"><a href="/admin/app"><i class="icon-fw icon-settings"></i> {{ t('App settings') }}</a></li>
+  <li class="{% if current == 'security'%}active{% endif %}"><a href="/admin/security"><i class="icon-fw icon-shield"></i> {{ t('Security settings') }}</a></li>
+  <li class="{% if current == 'markdown'%}active{% endif %}"><a href="/admin/markdown"><i class="icon-fw icon-note"></i> {{ t('Markdown settings') }}</a></li>
+  <li class="{% if current == 'customize'%}active{% endif %}"><a href="/admin/customize"><i class="icon-fw icon-wrench"></i> {{ t('Customize') }}</a></li>
+  <li class="{% if current == 'notification'%}active{% endif %}"><a href="/admin/notification"><i class="icon-fw icon-bell"></i> {{ t('Notification settings') }}</a></li>
+  <li class="{% if current == 'user' || current == 'external-account' %}active{% endif %}"><a href="/admin/users"><i class="icon-fw icon-user"></i> {{ t('User management') }}</a></li>
+  <li class="{% if current == 'user-group'%}active{% endif %}"><a href="/admin/user-groups"><i class="icon-fw icon-people"></i> {{ t('UserGroup management') }}</a></li>
   {% if searchConfigured() %}
-  <li class="{% if current == 'search'%}active{% endif %}"><a href="/admin/search"><i class="fa fa-search"></i> 検索管理</a></li>
+  <li class="{% if current == 'search'%}active{% endif %}"><a href="/admin/search"><i class="icon-fw icon-magnifier"></i> 検索管理</a></li>
   {% endif %}
 </ul>

+ 5 - 5
lib/views/admin/widget/passport/google-oauth.html

@@ -19,7 +19,7 @@
       <li>
         Create App from <a href="https://api.slack.com/applications/new">this link</a>, and fill the form out as below:
         <dl class="dl-horizontal">
-          <dt>App Name</dt> <dd><code>crowi-plus</code> </dd>
+          <dt>App Name</dt> <dd><code>growi</code> </dd>
           <dt>Development Slack Team</dt> <dd>Select the team you want to notify to.</dd>
         </dl>
       </li>
@@ -44,7 +44,7 @@
     Set Permission Scopes to the App
     <ol>
       <li>Go to "OAuth &amp; Permissions" page.</li>
-      <li>Add "Send messages as crowi-plus"(<code>chat:write:bot</code>).</li>
+      <li>Add "Send messages as GROWI"(<code>chat:write:bot</code>).</li>
       <li>Don't forget to <strong>save</strong>.</li>
     </ol>
   </li>
@@ -63,7 +63,7 @@
   <li>
     (At Team) Approve the app
     <ol>
-      <li>Go to the management Apps page for the team you installed the app and approve crowi-plus.</li>
+      <li>Go to the management Apps page for the team you installed the app and approve "growi".</li>
     </ol>
   </li>
   <li>
@@ -73,10 +73,10 @@
     </ol>
   </li>
   <li>
-    (At crowi-plus) Input "clientId" and "clientSecret" and submit on this page.
+    (At GROWI admin page) Input "clientId" and "clientSecret" and submit on this page.
   </li>
   <li>
-    (At crowi-plus) Click "Connect to Slack" button to start OAuth process.
+    (At GROWI admin page) Click "Connect to Slack" button to start OAuth process.
   </li>
 </ol>
 {% endif %}

+ 7 - 7
lib/views/admin/widget/passport/ldap.html

@@ -9,13 +9,13 @@
       <label for="{{nameForIsLdapEnabled}}" class="col-xs-3 control-label">Use LDAP</label>
       <div class="col-xs-6">
         <div class="btn-group btn-toggle" data-toggle="buttons">
-          <label class="btn btn-default {% if isLdapEnabled %}active{% endif %}" data-active-class="primary">
+          <label class="btn btn-default btn-rounded btn-outline {% if isLdapEnabled %}active{% endif %}" data-active-class="primary">
             <input name="{{nameForIsLdapEnabled}}" value="true" type="radio"
-                {% if true === isLdapEnabled %}checked{% endif %}> Enable
+                {% if true === isLdapEnabled %}checked{% endif %}> ON
           </label>
-          <label class="btn btn-default {% if !isLdapEnabled %}active{% endif %}" data-active-class="primary">
+          <label class="btn btn-default btn-rounded btn-outline {% if !isLdapEnabled %}active{% endif %}" data-active-class="default">
             <input name="{{nameForIsLdapEnabled}}" value="false" type="radio"
-                {% if !isLdapEnabled %}checked{% endif %}> Disable
+                {% if !isLdapEnabled %}checked{% endif %}> OFF
           </label>
         </div>
       </div>
@@ -43,11 +43,11 @@
         <label for="{{nameForIsUserBind}}" class="col-xs-3 control-label">Binding Mode</label>
         <div class="col-xs-6">
           <div class="btn-group btn-toggle" data-toggle="buttons">
-            <label class="btn btn-default {% if !isUserBind %}active{% endif %}" data-active-class="primary">
+            <label class="btn btn-default btn-rounded btn-outline {% if !isUserBind %}active{% endif %}" data-active-class="primary">
               <input name="{{nameForIsUserBind}}" value="false" type="radio"
                   {% if !isUserBind %}checked{% endif %}> Manager Bind
             </label>
-            <label class="btn btn-default {% if isUserBind %}active{% endif %}" data-active-class="primary">
+            <label class="btn btn-default btn-rounded btn-outline {% if isUserBind %}active{% endif %}" data-active-class="primary">
               <input name="{{nameForIsUserBind}}" value="true" type="radio"
                   {% if isUserBind %}checked{% endif %}> User Bind
             </label>
@@ -284,7 +284,7 @@
 
       <div class="modal-header">
         <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
-        <h4 class="modal-title">{{ t('Test LDAP Account') }}</h4>
+        <div class="modal-title">{{ t('Test LDAP Account') }}</div>
       </div>
 
       <div class="modal-body">

+ 7 - 0
lib/views/admin/widget/theme-colorbox.html

@@ -0,0 +1,7 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
+  <g>
+    <path class="color1" d="M -1 -1 L65 -1 L65 65 L-1 65 L-1 -1 Z" fill="#ccc"></path>
+    <path class="color2" d="M -1 -1 L65 -1 L65 15 L-1 15 L-1 -1 Z" fill="#000"></path>
+    <path class="color3" d="M 44 15 L65 15 L65 65 L44 65 L44 15 Z" fill="#300"></path>
+  </g>
+</svg>

+ 0 - 48
lib/views/crowi-plus/base/not_found_nosidebar.html

@@ -1,48 +0,0 @@
-{% extends '../../not_found.html' %}
-
-
-{% block layout_sidebar %}
-{% endblock %}
-
-
-{% block layout_main %}
-<div id="main" class="main col-md-12 {% if page %}{{ css.grant(page) }}{% endif %} {% block main_css_class %}{% endblock %}">
-  {% if page && page.grant != 1 %}
-  <p class="page-grant">
-    <i class="fa fa-lock"></i> {{ consts.pageGrants[page.grant] }} ({{ t('Browsing of this page is restricted') }})
-  </p>
-  {% endif %}
-  {% if page && page.grant == 2 %}
-  <p class="alert alert-info">
-    {{ t('Shareable Link') }}
-    <input type="text" class="copy-link form-control" value="{{ baseUrl }}/{{ page._id.toString() }}" readonly>
-  </p>
-  {% endif %}
-  <article>
-    {% block content_head %}
-      {% parent %}
-    {% endblock %}
-
-    {% block content_main_before %}
-    {% endblock %}
-
-    {% block content_main %}
-      {% parent %}
-    {% endblock content_main %}
-
-    {% block content_main_after %}
-    {% endblock %}
-
-    {% block content_footer %}
-      {% parent %}
-    {% endblock %}
-  </article>
-</div>
-
-{% endblock %} {# layout_main #}
-
-
-{% block footer %}
-  {% parent %}
-  {% include '../widget/system-version.html' %}
-{% endblock %}

+ 0 - 48
lib/views/crowi-plus/base/page_list_nosidebar.html

@@ -1,48 +0,0 @@
-{% extends '../../page_list.html' %}
-
-
-{% block layout_sidebar %}
-{% endblock %}
-
-
-{% block layout_main %}
-<div id="main" class="main col-md-12 {% if page %}{{ css.grant(page) }}{% endif %} {% block main_css_class %}{% endblock %}">
-  {% if page && page.grant != 1 %}
-  <p class="page-grant">
-    <i class="fa fa-lock"></i> {{ consts.pageGrants[page.grant] }} ({{ t('Browsing of this page is restricted') }})
-  </p>
-  {% endif %}
-  {% if page && page.grant == 2 %}
-  <p class="alert alert-info">
-    {{ t('Shareable Link') }}
-    <input type="text" class="copy-link form-control" value="{{ baseUrl }}/{{ page._id.toString() }}" readonly>
-  </p>
-  {% endif %}
-  <article>
-    {% block content_head %}
-      {% parent %}
-    {% endblock %}
-
-    {% block content_main_before %}
-    {% endblock %}
-
-    {% block content_main %}
-      {% parent %}
-    {% endblock content_main %}
-
-    {% block content_main_after %}
-    {% endblock %}
-
-    {% block content_footer %}
-      {% parent %}
-    {% endblock %}
-  </article>
-</div>
-
-{% endblock %} {# layout_main #}
-
-
-{% block footer %}
-  {% parent %}
-  {% include '../widget/system-version.html' %}
-{% endblock %}

+ 0 - 48
lib/views/crowi-plus/base/page_nosidebar.html

@@ -1,48 +0,0 @@
-{% extends '../../page.html' %}
-
-
-{% block layout_sidebar %}
-{% endblock %}
-
-
-{% block layout_main %}
-<div id="main" class="main col-md-12 {% if page %}{{ css.grant(page) }}{% endif %} {% block main_css_class %}{% endblock %}">
-  {% if page && page.grant != 1 %}
-  <p class="page-grant">
-    <i class="fa fa-lock"></i> {{ consts.pageGrants[page.grant] }} ({{ t('Browsing of this page is restricted') }})
-  </p>
-  {% endif %}
-  {% if page && page.grant == 2 %}
-  <p class="alert alert-info">
-    {{ t('Shareable Link') }}
-    <input type="text" class="copy-link form-control" value="{{ baseUrl }}/{{ page._id.toString() }}" readonly>
-  </p>
-  {% endif %}
-  <article>
-    {% block content_head %}
-      {% parent %}
-    {% endblock %}
-
-    {% block content_main_before %}
-    {% endblock %}
-
-    {% block content_main %}
-      {% parent %}
-    {% endblock content_main %}
-
-    {% block content_main_after %}
-    {% endblock %}
-
-    {% block content_footer %}
-      {% parent %}
-    {% endblock %}
-  </article>
-</div>
-
-{% endblock %} {# layout_main #}
-
-
-{% block footer %}
-  {% parent %}
-  {% include '../widget/system-version.html' %}
-{% endblock %}

+ 0 - 72
lib/views/crowi-plus/base/user_page_nosidebar.html

@@ -1,72 +0,0 @@
-{% extends '../../user_page.html' %}
-
-
-{% block layout_sidebar %}
-{% endblock %}
-
-
-{% block layout_main %}
-<div id="main" class="main col-md-12 {% if page %}{{ css.grant(page) }}{% endif %} {% block main_css_class %}{% endblock %}">
-  {% if page && page.grant != 1 %}
-  <p class="page-grant">
-    <i class="fa fa-lock"></i> {{ consts.pageGrants[page.grant] }} ({{ t('Browsing of this page is restricted') }})
-  </p>
-  {% endif %}
-  {% if page && page.grant == 2 %}
-  <p class="alert alert-info">
-    {{ t('Shareable Link') }}
-    <input type="text" class="copy-link form-control" value="{{ baseUrl }}/{{ page._id.toString() }}" readonly>
-  </p>
-  {% endif %}
-  <article>
-    <div class="container-fluid">
-      <div class="row">
-
-        <div class="col-lg-10 col-md-9">
-
-          {#
-           # ensure to insert 'content_head' block to col-xx-xx
-           #
-           #   Because this block has content like 'Bookmarks' or 'Recent Created' whose height changes dynamically,
-           #   setting of 'revision-toc' (affix) is hindered.
-           #}
-
-          {% block content_head %}
-            {% parent %}
-          {% endblock %}
-
-          {% block content_main_before %}
-          {% endblock %}
-
-          {% block content_main %}
-            {% parent %}
-          {% endblock content_main %}
-
-        </div> {# /.col- #}
-
-        {# relocate #revision-toc #}
-        <div class="col-lg-2 col-md-3 visible-lg visible-md">
-          <div id="revision-toc" class="revision-toc" data-spy="affix" data-offset-top="54">
-            <div id="revision-toc-content" class="revision-toc-content"></div>
-          </div>
-        </div> {# /.col- #}
-
-      </div>
-    </div>
-
-    {% block content_main_after %}
-    {% endblock %}
-
-    {% block content_footer %}
-      {% parent %}
-    {% endblock %}
-  </article>
-</div>
-
-{% endblock %} {# layout_main #}
-
-
-{% block footer %}
-  {% parent %}
-  {% include '../widget/system-version.html' %}
-{% endblock %}

+ 0 - 34
lib/views/crowi-plus/not_found.html

@@ -1,34 +0,0 @@
-{% extends 'base/not_found_nosidebar.html' %}
-
-{% block main_css_class %}
-  main-crowi-plus-customized
-  {% parent %}
-{% endblock %}
-
-{% block content_head %}
-
-  {% block content_head_before %}
-  {% endblock %}
-
-  {% include 'widget/header.html' %}
-
-  {% block content_head_after %}
-  {% endblock %}
-
-{% endblock %} {# /content_head #}
-
-
-{% block content_main %}
-  <div class="container-fluid">
-    <div class="row">
-
-      <div class="col-lg-10 col-md-9">
-
-        {% parent %}
-
-      </div> {# /.col- #}
-
-    </div>
-
-  </div>
-{% endblock %}

+ 0 - 57
lib/views/crowi-plus/page.html

@@ -1,57 +0,0 @@
-{% extends 'base/page_nosidebar.html' %}
-
-{% block main_css_class %}
-  main-crowi-plus-customized
-  {% parent %}
-{% endblock %}
-
-{% block content_head %}
-
-  {% block content_head_before %}
-  {% endblock %}
-
-  {% include 'widget/header.html' %}
-
-  {% block content_head_after %}
-  {% endblock %}
-
-{% endblock %} {# /content_head #}
-
-{% block content_main %}
-  <div class="container-fluid">
-    <div class="row">
-
-      <div class="col-lg-10 col-md-9">
-
-        {% parent %}
-
-        {# force remove #revision-toc from #content_main of parent #}
-        <script>
-          $('#revision-toc').remove();
-        </script>
-
-      </div> {# /.col- #}
-
-      {# relocate #revision-toc #}
-      <div class="col-lg-2 col-md-3 visible-lg visible-md">
-        <div id="revision-toc" class="revision-toc" data-spy="affix" data-offset-top="128">
-          <div id="revision-toc-content" class="revision-toc-content"></div>
-        </div>
-      </div> {# /.col- #}
-
-    </div>
-
-    {% if 'crowi-plus' === behaviorType() %}
-    <div class="row page-list">
-      <div class="col-md-12">
-        {% include './widget/page_list_container.html' %}
-      </div>
-    </div>
-    {% endif %}
-
-  </div>
-{% endblock %}
-
-{% block content_main_after %}
-  {% include 'widget/comments.html' %}
-{% endblock %}

+ 0 - 48
lib/views/crowi-plus/page_list.html

@@ -1,48 +0,0 @@
-{% extends 'base/page_list_nosidebar.html' %}
-
-{% block main_css_class %}
-  main-crowi-plus-customized
-  {% parent %}
-{% endblock %}
-
-{% block content_head %}
-
-  {% block content_head_before %}
-  {% endblock %}
-
-  {% include 'widget/header.html' %}
-
-  {% block content_head_after %}
-  {% endblock %}
-
-{% endblock %} {# /content_head #}
-
-
-{% block content_main %}
-  <div class="container-fluid">
-    <div class="row">
-
-      <div class="col-lg-10 col-md-9">
-
-        {% parent %}
-
-        {# force remove #revision-toc from #content_main of parent #}
-        <script>
-          $('#revision-toc').remove();
-
-          // hide unportalize button
-          $('.portal > .nav > .dropdown').remove();
-        </script>
-
-      </div> {# /.col- #}
-
-      {# relocate #revision-toc #}
-      <div class="col-lg-2 col-md-3 visible-lg visible-md">
-        <div id="revision-toc" class="revision-toc" data-spy="affix" data-offset-top="100">
-          <div id="revision-toc-content" class="revision-toc-content"></div>
-        </div>
-      </div> {# /.col- #}
-
-    </div>
-  </div>
-{% endblock %}

+ 0 - 24
lib/views/crowi-plus/user_page.html

@@ -1,24 +0,0 @@
-{% extends 'base/user_page_nosidebar.html' %}
-
-{% block main_css_class %}
-  main-crowi-plus-customized
-  {% parent %}
-{% endblock %}
-
-{% block content_head %}
-  {% parent %}
-{% endblock %} {# /content_head #}
-
-
-{% block content_main %}
-  {% parent %}
-
-  {# force remove #revision-toc from #content_main of parent #}
-  <script>
-    $('#revision-toc').remove();
-  </script>
-{% endblock %}
-
-{% block content_main_after %}
-  {% include 'widget/comments.html' %}
-{% endblock %}

+ 0 - 69
lib/views/crowi-plus/widget/header.html

@@ -1,69 +0,0 @@
-<div class="header-wrap">
-  <header id="page-header">
-    <div class="flex-title-line">
-      <div class="title-logo-container hidden-xs hidden-sm">
-        <a href="/">
-          <img alt="Crowi" src="/logo/32x32_g.png" />
-        </a>
-      </div>
-      <div class="title-container">
-        <h1 class="title flex-item-title" id="revision-path"></h1>
-        <div id="revision-url" class="url-line"></div>
-      </div>
-      {% if page %}
-      <div class="flex-item-action">
-        {% if user %}
-        <button
-            data-csrftoken="{{ csrf() }}"
-            data-liked="{% if page.isLiked(user) %}1{% else %}0{% endif %}"
-            class="like-button btn btn-default btn-sm {% if page.isLiked(user) %}active{% endif %}"
-        ><i class="fa fa-thumbs-o-up"></i></button>
-        {% endif %}
-      </div>
-      <div class="flex-item-action">
-        {% if user %}
-        <span id="bookmark-button">
-          <p class="bookmark-link">
-            <i class="fa fa-star-o"></i>
-          </p>
-        </span>
-        {% endif %}
-      </div>
-
-      <ul class="authors visible-md visible-lg">
-        <li>
-          <div class="creator-picture">
-            <a href="{{ userPageRoot(page.creator) }}">
-              <img src="{{ page.creator|default(author)|picture }}" class="picture picture-rounded"><br>
-            </a>
-          </div>
-          <div class="">
-            <div>Created by <a href="{{ userPageRoot(page.creator) }}">{{ page.creator.name|default(author.name) }}</a></div>
-            <div class="text-muted">{{ page.createdAt|datetz('Y/m/d H:i:s') }}</div>
-          </div>
-        </li>
-        <li>
-          <div class="creator-picture">
-            <a href="{{ userPageRoot(page.lastUpdateUser) }}">
-              <img src="{{ page.lastUpdateUser|default(author)|picture }}" class="picture picture-rounded"><br>
-            </a>
-          </div>
-          <div class="">
-            <div>Updated by <a href="{{ userPageRoot(page.lastUpdateUser) }}">{{ page.lastUpdateUser.name|default(author.name) }}</a></div>
-            <div class="text-muted">{{ page.updatedAt|datetz('Y/m/d H:i:s') }}</div>
-          </div>
-        </li>
-      </ul>
-      {% endif %}
-
-      {% if not page and ('/' === path or 'crowi' === behaviorType()) and not isUserPageList(path) and !isTrashPage() %}
-      <div class="portal-form-button">
-        <button class="btn btn-primary" id="create-portal-button" {% if not user %}disabled{% endif %}>Create Portal</button>
-        <p class="help-block"><a href="#" data-target="#help-portal" data-toggle="modal"><i class="fa fa-question-circle"></i> What is Portal?</a></p>
-      </div>
-      {% endif %}
-
-    </div>
-
-  </header>
-</div>

+ 3 - 3
lib/views/customlayout-selector/not_found.html

@@ -1,5 +1,5 @@
-{% if 'crowi-plus' === layoutType() %}
-  {% include '../crowi-plus/not_found.html' %}
+{% if !layoutType() || 'crowi' === layoutType() %}
+  {% include '../layout-crowi/not_found.html' %}
 {% else %}
-  {% include '../not_found.html' %}
+  {% include '../layout-growi/not_found.html' %}
 {% endif %}

+ 3 - 3
lib/views/customlayout-selector/page.html

@@ -1,5 +1,5 @@
-{% if 'crowi-plus' === layoutType() %}
-  {% include '../crowi-plus/page.html' %}
+{% if !layoutType() || 'crowi' === layoutType() %}
+  {% include '../layout-crowi/page.html' %}
 {% else %}
-  {% include '../page.html' %}
+  {% include '../layout-growi/page.html' %}
 {% endif %}

+ 3 - 3
lib/views/customlayout-selector/page_list.html

@@ -1,5 +1,5 @@
-{% if 'crowi-plus' === layoutType() %}
-  {% include '../crowi-plus/page_list.html' %}
+{% if !layoutType() || 'crowi' === layoutType() %}
+  {% include '../layout-crowi/page_list.html' %}
 {% else %}
-  {% include '../page_list.html' %}
+  {% include '../layout-growi/page_list.html' %}
 {% endif %}

+ 3 - 3
lib/views/customlayout-selector/user_page.html

@@ -1,5 +1,5 @@
-{% if 'crowi-plus' === layoutType() %}
-  {% include '../crowi-plus/user_page.html' %}
+{% if !layoutType() || 'crowi' === layoutType() %}
+  {% include '../layout-crowi/user_page.html' %}
 {% else %}
-  {% include '../user_page.html' %}
+  {% include '../layout-growi/user_page.html' %}
 {% endif %}

+ 0 - 15
lib/views/index.html

@@ -1,15 +0,0 @@
-{% extends 'layout/2column.html' %}
-
-{% block content_head %}
-<header>
-  <h2>Index</h2>
-</header>
-{% endblock %}
-
-{% block content_main %}
-
-  {% for page in pages %}
-    <a href="/{{ page.path }}">{{ page.path }} ({{page.updatedAt|date('Y-m-d H:i:s O')}})</a><br />
-  {% endfor %}
-
-{% endblock %}

+ 82 - 58
lib/views/installer.html

@@ -1,89 +1,113 @@
-{% extends 'layout/single-nologin.html' %}
+{% extends 'layout/layout.html' %}
+
+{% block html_base_css %}installer nologin{% endblock %}
 
 {% block html_title %}セットアップ {% endblock %}
 
-{% block content_main %}
 
 
-<div class="login-dialog-container col-md-5">
+{#
+ # Remove default contents
+ #}
+{% block html_head_loading_legacy %}
+{% endblock %}
+{% block html_head_loading_app %}
+{% endblock %}
+{% block layout_head_nav %}
+{% endblock %}
+{% block sidebar %}
+{% endblock %}
 
-<div class="installer-header">
-  <img src="/logo/135x32.png" alt="Crowi">
-  <h1>
-    Crowi のセットアップへようこそ!
-  </h1>
-</div>
 
 
-<div class="login-dialog"  id="login-dialog">
-  <div class="login-dialog-inner">
-    <h2>管理者の作成</h2>
+{% block layout_main %}
 
-    <p class="text-info">
-    はじめに、管理者アカウントを作成してください。
-    </p>
+<div class="main container-fluid">
 
-    {% if req.form.errors.length > 0 %}
-    <div class="alert alert-danger">
-      <ul>
-      {% for error in req.form.errors %}
-        <li>{{ error }}</li>
-      {% endfor %}
-      </ul>
-    </div>
-    {% endif %}
+  <div class="row">
 
-    <form role="form" action="/installer/createAdmin" method="post">
-      <label>ユーザーID</label>
-      <div class="input-group" id="input-group-username">
-        <span class="input-group-addon"><strong>@</strong></span>
-        <input type="text" class="form-control" placeholder="記入例: taroyama" name="registerForm[username]" value="{{ req.body.registerForm.username }}" required>
-      </div>
-      <p class="help-block">
-      <span id="help-block-username" class="text-danger"></span>
-      ユーザーIDは、ユーザーページのURLなどに利用されます。半角英数字と一部の記号のみ利用できます。
-      </p>
-
-      <label>名前</label>
-      <div class="input-group">
-        <span class="input-group-addon"><i class="fa fa-user"></i></span>
-        <input type="text" class="form-control" placeholder="記入例: 山田 太郎" name="registerForm[name]" value="{{ req.body.registerForm.name }}" required>
-      </div>
+    <div class="login-header col-sm-offset-4 col-sm-4">
+      <div class="logo">{% include 'widget/logo.html' %}</div>
+      <h1>GROWI</h1>
 
-      <label >メールアドレス</label>
-      <div class="input-group">
-        <span class="input-group-addon"><i class="fa fa-envelope"></i></span>
-        <input type="email" class="form-control" placeholder="E-mail" name="registerForm[email]" value="{{ googleEmail|default(req.body.registerForm.email) }}" required>
+      <div class="login-form-errors">
+        {% if req.form.errors.length > 0 %}
+        <div class="alert alert-danger">
+          <ul>
+          {% for error in req.form.errors %}
+            <li>{{ error }}</li>
+          {% endfor %}
+          </ul>
+        </div>
+        {% endif %}
       </div>
+    </div>
 
-      <label>パスワード</label>
-      <div class="input-group">
-        <span class="input-group-addon"><i class="fa fa-key"></i></span>
-        <input type="password" class="form-control" placeholder="Password" name="registerForm[password]" required>
-      </div>
-      <p class="help-block">
-      パスワードは6文字以上の半角英数字または記号
+    <div class="login-dialog p-t-10 p-b-10 col-sm-offset-4 col-sm-4" id="login-dialog">
+      <p class="alert alert-success">
+        <strong>最初のアカウントの作成</strong><br>
+        <small>初めに作成するアカウントは、自動的に管理者権限が付与されます</small>
       </p>
 
-      <input type="hidden" name="_csrf" value="{{ csrf() }}">
-      <input type="submit" class="btn btn-primary btn-lg btn-block" value="作成">
-    </form>
+      <form role="form" action="/installer/createAdmin" method="post" id="register-form">
+
+        <div class="input-group" id="input-group-username">
+          <span class="input-group-addon"><i class="icon-user"></i></span>
+          <input type="text" class="form-control" placeholder="{{ t('User ID') }}" name="registerForm[username]" value="{{ req.body.registerForm.username }}" required>
+        </div>
+        <p class="help-block">
+          <span id="help-block-username"></span>
+        </p>
+
+        <div class="input-group">
+          <span class="input-group-addon"><i class="icon-tag"></i></span>
+          <input type="text" class="form-control" placeholder="{{ t('Name') }}" name="registerForm[name]" value="{{ googleName|default(req.body.registerForm.name) }}" required>
+        </div>
+
+        <div class="input-group">
+          <span class="input-group-addon"><i class="icon-envelope"></i></span>
+          <input type="email" class="form-control" placeholder="{{ t('Email') }}" name="registerForm[email]" value="{{ googleEmail|default(req.body.registerForm.email) }}" required>
+        </div>
+
+        <div class="input-group">
+          <span class="input-group-addon"><i class="icon-lock"></i></span>
+          <input type="password" class="form-control" placeholder="{{ t('Password') }}" name="registerForm[password]" required>
+        </div>
+
+        <input type="hidden" name="_csrf" value="{{ csrf() }}">
+        <div class="input-group m-t-30 m-b-20 d-flex justify-content-center">
+          <button type="submit" class="fcbtn btn btn-success btn-1b btn-register">
+            <span class="btn-label"><i class="icon-user-follow"></i></span>
+            {{ t('Create') }}
+          </button>
+        </div>
+
+        <div class="input-group m-t-30 d-flex justify-content-center">
+          <a href="https://growi.org" class="link-growi-org">
+            <span class="growi">GROWI</span>.<span class="org">ORG
+          </a>
+        </div>
+      </form>
+    </div>
 
-  </div>
-</div>
+  </div>{# /.row #}
 
-</div>
+</div>{# /.main #}
 
 <script>
 $(function() {
   $('#register-form input[name="registerForm[username]"]').change(function(e) {
     var username = $(this).val();
+    $('#login-dialog').removeClass('has-error');
     $('#input-group-username').removeClass('has-error');
     $('#help-block-username').html("");
 
     $.getJSON('/_api/check_username', {username: username}, function(json) {
       if (!json.valid) {
-        $('#help-block-username').html('<i class="fa fa-warning"></i>このユーザーIDは利用できません。<br>');
+        $('#help-block-username').html(
+          '<i class="icon-fw icon-ban"></i>このユーザーIDは利用できません。'
+        );
+        $('#login-dialog').addClass('has-error');
         $('#input-group-username').addClass('has-error');
       }
     });

+ 90 - 74
lib/views/invited.html

@@ -1,111 +1,127 @@
-{% extends 'layout/single-nologin.html' %}
+{% extends 'layout/layout.html' %}
+
+{% block html_base_css %}invited nologin{% endblock %}
 
 {% block html_title %}Registration · {% endblock %}
 
-{% block content_main %}
 
-<h1 class="login-page">
-  {% if config.crowi['app:title'] == 'Crowi' %}
-    <img src="/logo/135x32.png" alt="Crowi">
-  {% else %}
-    {{ config.crowi['app:title'] }}<br>
-    <img src="/logo/100x11_w.png" alt="powered by Crowi">
-  {% endif %}
-</h1>
 
-<div class="login-dialog-container flip-container col-md-5">
+{#
+  # Remove default contents
+  #}
+ {% block html_head_loading_legacy %}
+ {% endblock %}
+ {% block html_head_loading_app %}
+ {% endblock %}
+ {% block layout_head_nav %}
+ {% endblock %}
+ {% block sidebar %}
+ {% endblock %}
 
-<div class="login-dialog" id="login-dialog">
 
-  <div class="login-dialog-inner front">
-    <h2>ユーザー情報入力</h2>
 
-    <p>
-    ようこそ!<br>
-    はじめに、あなたのことを教えて下さい。
-    </p>
+ {% block layout_main %}
 
-    <div id="login-form-errors">
-      {% set message = req.flash('warningMessage') %}
-      {% if message.length %}
-      <div class="alert alert-danger">
-        {{ message }}
-      </div>
-      {% endif %}
-
-      {% if req.form.errors.length > 0 %}
-      <div class="alert alert-danger">
-        <ul>
-        {% for error in req.form.errors %}
-          <li>{{ error }}</li>
-        {% endfor %}
-        </ul>
-      </div>
-      {% endif %}
-    </div>
-    <form role="form" id="invited-form" action="/login/activateInvited" method="post">
+<div class="main container-fluid">
 
-      <label>メールアドレス</label>
-      <div class="input-group">
-        <span class="input-group-addon"><i class="fa fa-envelope"></i></span>
-        <input type="text" class="form-control" disabled value="{{ user.email }}">
-      </div>
-      <p class="help-block">
-      このメールアドレスで招待を受け取っています。
-      </p>
+  <div class="row">
 
-      <label>ユーザーID</label>
-      <div class="input-group" id="input-group-username">
-        <span class="input-group-addon"><strong>@</strong></span>
-        <input type="text" class="form-control" placeholder="記入例: taroyama" name="invitedForm[username]" value="{{ req.body.invitedForm.username }}" required>
-      </div>
-      <p class="help-block">
-      <span id="help-block-username" class="text-danger"></span>
-      ユーザーIDは、ユーザーページのURLなどに利用されます。半角英数字と一部の記号のみ利用できます。
-      </p>
+    <div class="login-header col-sm-offset-4 col-sm-4">
+      <div class="logo">{% include 'widget/logo.html' %}</div>
+      <h1>GROWI</h1>
 
-      <label>名前</label>
-      <div class="input-group">
-        <span class="input-group-addon"><i class="fa fa-user"></i></span>
-        <input type="text" class="form-control" placeholder="記入例: 山田 太郎" name="invitedForm[name]" value="{{ req.body.invitedForm.name }}" required>
-      </div>
+      <div id="login-form-errors">
+        {% set message = req.flash('warningMessage') %}
+        {% if message.length %}
+        <div class="alert alert-danger">
+          {{ message }}
+        </div>
+        {% endif %}
 
-      <label>パスワード</label>
-      <div class="input-group">
-        <span class="input-group-addon"><i class="fa fa-key"></i></span>
-        <input type="password" class="form-control" placeholder="Password" name="invitedForm[password]" required>
+        {% if req.form.errors.length > 0 %}
+        <div class="alert alert-danger">
+          <ul>
+          {% for error in req.form.errors %}
+            <li>{{ error }}</li>
+          {% endfor %}
+          </ul>
+        </div>
+        {% endif %}
       </div>
-      <p class="help-block">
-      現在、仮パスワードでログインしています。新しいパスワードを決定してください。<br>
-      パスワードは6文字以上の半角英数字または記号
+    </div>
+
+    <div class="login-dialog p-t-10 p-b-10 col-sm-offset-4 col-sm-4" id="login-dialog">
+      <p class="alert alert-success">
+        <strong>アカウントの作成</strong><br>
+        <small>招待を受け取ったメールアドレスでアカウントを作成します</small>
       </p>
 
-      <input type="hidden" name="_csrf" value="{{ csrf() }}">
-      <input type="submit" class="btn btn-primary btn-lg btn-block" value="登録を完了">
-    </form>
+      <form role="form" action="/installer/activateInvited" method="post" id="invited-form">
+
+        <div class="input-group">
+          <span class="input-group-addon"><i class="icon-envelope"></i></span>
+          <input type="text" class="form-control" disabled value="{{ user.email }}">
+        </div>
+
+        <div class="input-group" id="input-group-username">
+          <span class="input-group-addon"><i class="icon-user"></i></span>
+          <input type="text" class="form-control" placeholder="{{ t('User ID') }}" name="invitedForm[username]" value="{{ req.body.invitedForm.username }}" required>
+        </div>
+        <p class="help-block">
+          <span id="help-block-username"></span>
+        </p>
+
+        <div class="input-group">
+          <span class="input-group-addon"><i class="icon-tag"></i></span>
+          <input type="text" class="form-control" placeholder="{{ t('Name') }}" name="invitedForm[name]" value="{{ googleName|default(req.body.invitedForm.name) }}" required>
+        </div>
+
+
+        <div class="input-group">
+          <span class="input-group-addon"><i class="icon-lock"></i></span>
+          <input type="password" class="form-control" placeholder="{{ t('Password') }}" name="invitedForm[password]" required>
+        </div>
+
+        <input type="hidden" name="_csrf" value="{{ csrf() }}">
+        <div class="input-group m-t-30 m-b-20 d-flex justify-content-center">
+          <button type="submit" class="fcbtn btn btn-success btn-1b btn-register">
+            <span class="btn-label"><i class="icon-user-follow"></i></span>
+            {{ t('Create') }}
+          </button>
+        </div>
+
+        <div class="input-group m-t-30 d-flex justify-content-center">
+          <a href="https://growi.org" class="link-growi-org">
+            <span class="growi">GROWI</span>.<span class="org">ORG
+          </a>
+        </div>
+      </form>
+    </div>
 
-    <hr>
+  </div>{# /.row #}
 
-  </div>
-</div>
+</div>{# /.main #}
 
 <script>
 $(function() {
   $('#invited-form input[name="invitedForm[username]"]').change(function(e) {
     var username = $(this).val();
+    $('#login-dialog').removeClass('has-error');
     $('#input-group-username').removeClass('has-error');
     $('#help-block-username').html("");
 
     $.getJSON('/_api/check_username', {username: username}, function(json) {
       if (!json.valid) {
-        $('#help-block-username').html('<i class="fa fa-warning"></i>このユーザーIDは利用できません。<br>');
+        $('#help-block-username').html(
+          '<i class="icon-fw icon-ban"></i>このユーザーIDは利用できません。'
+        );
+        $('#login-dialog').addClass('has-error');
         $('#input-group-username').addClass('has-error');
       }
     });
   });
 });
 </script>
-</div>
 
 {% endblock %}
 

+ 51 - 0
lib/views/layout-crowi/base/layout.html

@@ -0,0 +1,51 @@
+{% extends '../../layout/layout.html' %}
+
+
+{% block layout_main %}
+<div class="container-fluid">
+
+  <a href="" class=" hidden-xs hidden-sm layout-control" id="toggle-sidebar">
+    <i class="ti-angle-right"></i><i class="ti-angle-left"></i> <span class="hide-on-affix-top"></span>
+  </a>
+  <aside class="crowi-sidebar col-md-3 hidden-xs hidden-sm hidden-print">
+
+    {% block side_header %}
+    {% endblock %}
+
+    <div class="side-content">
+      {% block side_content %}
+      {% endblock %}
+    </div>
+
+    {% block side_footer %}
+    {% endblock %}
+
+    {% include '../../widget/system-version.html' %}
+  </aside>
+
+  <div class="row bg-title">
+    <div class="col-md-9">
+      {% block content_header %}
+      {% endblock %}
+    </div>
+  </div><!-- /.bg-title -->
+
+  <div class="row">
+    <div id="main" class="main m-t-15 col-md-9 {% 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_after %}
+      {% endblock %}
+
+      {% block content_footer %}
+      {% endblock %}
+
+    </div>
+  </div>
+
+</div><!-- /.container-fluid -->
+{% endblock %} {# layout_main #}

+ 41 - 0
lib/views/layout-crowi/not_found.html

@@ -0,0 +1,41 @@
+{% extends 'base/layout.html' %}
+
+{% block content_header %}
+
+  {% block content_header_before %}
+  {% endblock %}
+
+  <div class="header-wrap">
+    <header id="page-header">
+      <div class="flex-title-line">
+        <div>
+          <h1 class="title flex-item-title" id="revision-path"></h1>
+          <div id="revision-url" class="url-line"></div>
+        </div>
+      </div>
+
+    </header>
+  </div>
+
+  {% block content_header_after %}
+  {% endblock %}
+
+{% endblock %} {# /content_head #}
+
+
+{% block content_main_before %}
+  {% include '../widget/page_alerts.html' %}
+{% endblock %}
+
+
+{% block content_main %}
+  {% include '../widget/not_found_content.html' %}
+{% endblock %}
+
+
+{% block content_main_after %}
+{% endblock %}
+
+
+{% block content_footer %}
+{% endblock %}

+ 68 - 0
lib/views/layout-crowi/page.html

@@ -0,0 +1,68 @@
+{% extends 'base/layout.html' %}
+
+
+{% block content_header %}
+
+  {% block content_header_before %}
+  {% endblock %}
+
+  <div class="header-wrap">
+    <header id="page-header">
+      <div class="d-flex align-items-center">
+        <div class="title-container">
+          <h1 class="title flex-item-title" id="revision-path"></h1>
+          <div id="revision-url" class="url-line"></div>
+        </div>
+        {% include '../widget/header-buttons.html' %}
+      </div>
+    </header>
+  </div>
+
+  {% block content_header_after %}
+  {% endblock %}
+
+{% endblock %}
+
+
+{% block content_main_before %}
+{% endblock %}
+
+
+{% block content_main %}
+  {% include '../widget/page_content.html' %}
+{% endblock %}
+
+
+{% block content_main_after %}
+{% endblock %}
+
+
+{% block content_footer %}
+  {% if page %}
+    {% include '../widget/page_attachments.html' %}
+  {% endif %}
+{% endblock %}
+
+{% block side_header %}
+  {% if page and not page.isDeleted() %}
+    {% include 'widget/page_side_header.html' %}
+  {% endif %}
+{% endblock %} {# side_header #}
+
+{% block side_content %}
+  {% if page and not page.isDeleted() %}
+    {% include 'widget/page_side_content.html' %}
+  {% endif %}
+{% endblock %}
+
+{% block layout_footer %}
+{% endblock %}
+
+{% block body_end %}
+  <div id="presentation-layer" class="fullscreen-layer">
+    <div id="presentation-container"></div>
+  </div>
+  <div id="crowi-modals">
+    {% include '../widget/page_modals.html' %}
+  </div>
+{% endblock %}

+ 98 - 0
lib/views/layout-crowi/page_list.html

@@ -0,0 +1,98 @@
+{% extends 'base/layout.html' %}
+
+
+{% block html_base_attr %}
+  data-spy="scroll"
+  data-target="#search-result-list"
+{% endblock %}
+
+{% block content_header %}
+
+{% block content_header_before %}
+{% endblock %}
+
+<div class="header-wrap">
+  <header id="page-header" class="{% if page %}has-page{% endif %}">
+
+    <div class="d-flex align-items-center">
+      <div class="title-container">
+        <div class="d-flex">
+          <h1 class="title flex-item-title" id="revision-path"></h1>
+          {% if false %} {# Disable temporaly -- 2018.03.08 Yuki Takei #}
+          {% if searchConfigured() && !isTopPage() && !isTrashPage() %}
+          <form id="search-listpage-form" class="m-l-10 input-group search-input-group hidden-xs hidden-sm"
+              data-toggle="tooltip" data-placement="bottom" title="{{ path }} 以下から検索" data-container="body">
+            <div class="input-group">
+              <input id="search-listpage-input" type="text" class="form-control input-sm" data-path="{{ path }}" placeholder="Search for...">
+              <span class="input-group-btn">
+                <button class="btn btn-default btn-sm"><i class="icon-magnifier"></i></button>
+              </span>
+            </div><!-- /input-group -->
+            <a class="search-listpage-clear" id="search-listpage-clear"><i class="fa fa-times-circle"></i></a>
+          </form>
+          {% endif %}
+          {% endif %}
+        </div>
+        <div id="revision-url" class="url-line"></div>
+      </div>
+      {% include '../widget/header-buttons.html' %}
+    </div>
+
+  </header>
+</div>
+
+{% endblock %}
+
+{% block content_main %}
+
+  {% block content_main_before %}
+  {% endblock %}
+
+  {# page-list-search should be fully managed by react.js,
+  # but now the header and page list content is rendered separately by the server,
+  # so now bind the values through the hidden fields.
+  #}
+  {% if false %} {# Disable temporaly -- 2018.03.08 Yuki Takei #}
+  {% if searchConfigured() && !isTopPage() && !isTrashPage() %}
+  <div id="page-list-search">
+  </div>
+  {% endif %}
+  {% endif %}
+
+  {% include '../widget/page_content.html' %}
+
+  <div class="row page-list m-t-30">
+    <div class="col-md-12">
+      {% include '../widget/page_list_and_timeline.html' %}
+    </div>
+  </div>
+
+{% endblock %}
+
+
+{% block content_main_after %}
+{% endblock %}
+
+
+{% block content_footer %}
+<footer>
+</footer>
+{% endblock %}
+
+
+{% block side_header %}
+
+{% if not page and not isUserPageList(path) and !isTrashPage() %}
+  {% include '../widget/create_portal.html' %}
+{% else %}
+  {% include 'widget/page_side_header.html' %}
+{% endif %}
+
+{% endblock %} {# side_header #}
+
+{% block body_end %}
+<div id="crowi-modals">
+  {% include '../modal/what_is_portal.html' %}
+  {% include '../modal/unportalize.html' %}
+</div>
+{% endblock %} {# body_end #}

+ 19 - 0
lib/views/layout-crowi/user_page.html

@@ -0,0 +1,19 @@
+{% extends 'page.html' %}
+
+{% block main_css_class %}user-page{% endblock %}
+
+
+{% block content_header %}
+  {% if pageUser %}
+    {% include '../widget/user_page_header.html' %}
+  {% else %}
+    {% parent %}
+  {% endif %}
+{% endblock %}
+
+
+{% block content_main_before %}
+  <div class="m-b-30 user-page-content-container">
+    {% include '../widget/user_page_content.html' %}
+  </div>
+{% endblock %}

+ 5 - 5
lib/views/widget/page_side_content.html → lib/views/layout-crowi/widget/page_side_content.html

@@ -1,8 +1,8 @@
-<h3><i class="fa fa-link"></i> {{ t('Share') }}</h3>
+<h3><i class="icon-link"></i> {{ t('Share') }}</h3>
 <ul class="fitted-list">
   <li class="input-group">
     <span class="input-group-addon">{{ t('Share Link') }}</span>
-    <input readonly class="copy-link form-control" type="text" value="{{ config.crowi['app:title']|default('Crowi') }} {{ path }}  {{ baseUrl }}/{{ page._id.toString() }}">
+    <input readonly class="copy-link form-control" type="text" value="{{ appTitle }} {{ path }} {{ baseUrl }}/{{ page._id.toString() }}">
   </li>
   <li class="input-group">
     <span class="input-group-addon">Markdown</span>
@@ -10,7 +10,7 @@
   </li>
 </ul>
 
-<h3><i class="fa fa-comment"></i> Comments</h3>
+<h3><i class="icon-bubble"></i> Comments</h3>
 <div class="page-comments">
   <form class="form page-comment-form" id="page-comment-form" onsubmit="return false;">
     <div class="comment-form">
@@ -33,11 +33,11 @@
   <div class="page-comments-list" id="page-comments-list">
     <div class="page-comments-list-newer collapse" id="page-comments-list-newer"></div>
 
-    <a class="page-comments-list-toggle-newer text-center" data-toggle="collapse" href="#page-comments-list-newer"><i class="fa fa-angle-double-up"></i> Comments for Newer Revision <i class="fa fa-angle-double-up"></i></a>
+    <a class="page-comments-list-toggle-newer text-center" data-toggle="collapse" href="#page-comments-list-newer"><i class="ti-angle-double-up"></i> Comments for Newer Revision <i class="ti-angle-double-up"></i></a>
 
     <div class="page-comments-list-current" id="page-comments-list-current"></div>
 
-    <a class="page-comments-list-toggle-older text-center" data-toggle="collapse" href="#page-comments-list-older"><i class="fa fa-angle-double-down"></i> Comments for Older Revision <i class="fa fa-angle-double-down"></i></a>
+    <a class="page-comments-list-toggle-older text-center" data-toggle="collapse" href="#page-comments-list-older"><i class="ti-angle-double-down"></i> Comments for Older Revision <i class="ti-angle-double-down"></i></a>
 
     <div class="page-comments-list-older collapse in" id="page-comments-list-older"></div>
   </div>

+ 6 - 6
lib/views/widget/page_side_header.html → lib/views/layout-crowi/widget/page_side_header.html

@@ -4,7 +4,7 @@
     {# default(author) としているのは、v1.1.1 以前に page.creator データが入ってないから。暫定として最新更新ユーザーを表示しちゃう。 #}
     <div class="col-md-3 creator-picture">
       <a href="{{ userPageRoot(page.creator) }}">
-        <img src="{{ page.creator|default(author)|picture }}" class="picture picture-lg picture-rounded"><br>
+        <img src="{{ page.creator|default(author)|picture }}" class="picture picture-lg img-circle"><br>
       </a>
     </div>
     <div class="col-md-9">
@@ -15,10 +15,10 @@
         {{ t('Created') }}: {{ page.createdAt|datetz('Y/m/d H:i:s') }}<br>
 
         {% if page.lastUpdateUser %}
-          {{ t('Last updated') }}: {{ page.updatedAt|datetz('Y/m/d H:i:s') }} <a href="/user/{{ page.lastUpdateUser.username }}"><img src="{{ page.lastUpdateUser|picture }}" class="picture picture-xs picture-rounded" alt="{{ page.lastUpdateUser.name }}"></a>
+          {{ t('Last updated') }}: {{ page.updatedAt|datetz('Y/m/d H:i:s') }} <a href="/user/{{ page.lastUpdateUser.username }}"><img src="{{ page.lastUpdateUser|picture }}" class="picture picture-xs img-circle" alt="{{ page.lastUpdateUser.name }}"></a>
         {% else %}
           {# for BC 1.5.x #}
-          {{ t('Last updated') }}: {{ page.updatedAt|datetz('Y/m/d H:i:s') }} <a href="/user/{{ page.revision.author.username }}"><img src="{{ page.revision.author|picture }}" class="picture picture-xs picture-rounded" alt="{{ page.revision.author.name }}"></a>
+          {{ t('Last updated') }}: {{ page.updatedAt|datetz('Y/m/d H:i:s') }} <a href="/user/{{ page.revision.author.username }}"><img src="{{ page.revision.author|picture }}" class="picture picture-xs img-circle" alt="{{ page.revision.author.name }}"></a>
         {% endif %}
       </p>
     </div>
@@ -27,7 +27,7 @@
   <div class="like-box">
     <dl class="dl-horizontal">
       <dt>
-        <i class="fa fa-thumbs-o-up"></i> {{ t('Like!') }}
+        <i class="icon-like"></i> {{ t('Like!') }}
       </dt>
       <dd>
         <p class="liker-count">
@@ -36,8 +36,8 @@
         <button
           data-csrftoken="{{ csrf() }}"
           data-liked="{% if page.isLiked(user) %}1{% else %}0{% endif %}"
-          class="like-button btn btn-default btn-sm {% if page.isLiked(user) %}active{% endif %}"
-          ><i class="fa fa-thumbs-o-up"></i> {{ t('Like!') }}</button>
+          class="like-button btn btn-xs btn-default btn-outline btn-rounded {% if page.isLiked(user) %}active btn-info{% endif %}"
+          ><i class="icon-like"></i> {{ t('Like!') }}</button>
         {% endif %}
         </p>
         <p id="liker-list" class="liker-list" data-likers="{{ page.liker|default([])|join(',') }}">

+ 33 - 0
lib/views/layout-growi/base/layout.html

@@ -0,0 +1,33 @@
+{% extends '../../layout/layout.html' %}
+
+
+{% block layout_main %}
+<div class="container-fluid">
+
+  <div class="row bg-title">
+    <div class="col-xs-12">
+      {% block content_header %}
+      {% endblock %}
+    </div>
+  </div><!-- /.bg-title -->
+
+  <div class="row">
+    <div id="main" class="main m-t-15 col-md-12 {% 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_after %}
+      {% endblock %}
+    </div>
+
+  </div>
+
+</div><!-- /.container-fluid -->
+
+<footer class="footer">
+  {% include '../../widget/system-version.html' %}
+</footer>
+{% endblock %} {# layout_main #}

+ 20 - 0
lib/views/layout-growi/not_found.html

@@ -0,0 +1,20 @@
+{% extends 'base/layout.html' %}
+
+
+{% block content_header %}
+  {% include 'widget/header.html' %}
+{% endblock %}
+
+
+{% block content_main_before %}
+  {% include '../widget/page_alerts.html' %}
+{% endblock %}
+
+
+{% block content_main %}
+  <div class="row">
+    <div class="col-lg-10 col-md-9">
+      {% include '../widget/not_found_content.html' %}
+    </div> {# /.col- #}
+  </div>
+{% endblock %}

+ 63 - 0
lib/views/layout-growi/page.html

@@ -0,0 +1,63 @@
+{% extends 'base/layout.html' %}
+
+
+{% block content_header %}
+  {% include 'widget/header.html' %}
+{% endblock %}
+
+
+{% block content_main_before %}
+{% endblock %}
+
+
+{% block content_main %}
+  <div class="row">
+
+    <div class="col-lg-10 col-md-9">
+
+      {% include '../widget/page_content.html' %}
+
+      {# force remove #revision-toc from #content_main of parent #}
+      <script>
+        $('#revision-toc').remove();
+      </script>
+
+    </div> {# /.col- #}
+
+    {# relocate #revision-toc #}
+    <div class="col-lg-2 col-md-3 revision-toc-container hidden-sm hidden-xs">
+      <div id="revision-toc" class="revision-toc" data-spy="affix" data-offset-top="80">
+        <div id="revision-toc-content" class="revision-toc-content"></div>
+      </div>
+    </div> {# /.col- #}
+
+  </div>
+
+  {% if 'growi' === behaviorType() || 'crowi-plus' === behaviorType() %}
+  <div class="row page-list m-t-30">
+    <div class="col-md-12">
+      {% include '../widget/page_list_and_timeline.html' %}
+    </div>
+  </div>
+  {% endif %}
+{% endblock %}
+
+
+{% block content_main_after %}
+  {% include 'widget/comments.html' %}
+
+  {% if page %}
+    {% include '../widget/page_attachments.html' %}
+  {% endif %}
+{% endblock %}
+
+
+{% block body_end %}
+  <div id="presentation-layer" class="fullscreen-layer">
+    <div id="presentation-container"></div>
+  </div>
+
+  <div id="crowi-modals">
+    {% include '../widget/page_modals.html' %}
+  </div>
+{% endblock %}

+ 60 - 0
lib/views/layout-growi/page_list.html

@@ -0,0 +1,60 @@
+{% extends 'base/layout.html' %}
+
+
+{% block content_header %}
+  {% include 'widget/header.html' %}
+{% endblock %}
+
+
+{% block content_main_before %}
+{% endblock %}
+
+
+{% block content_main %}
+  <div class="row">
+
+    <div class="col-lg-10 col-md-9">
+
+      {% include '../widget/page_content.html' %}
+
+      {# force remove #revision-toc from #content_main of parent #}
+      <script>
+        $('#revision-toc').remove();
+      </script>
+
+    </div> {# /.col- #}
+
+    {# relocate #revision-toc #}
+    <div class="col-lg-2 col-md-3 revision-toc-container hidden-sm hidden-xs">
+      <div id="revision-toc" class="revision-toc" data-spy="affix" data-offset-top="80">
+        <div id="revision-toc-content" class="revision-toc-content"></div>
+      </div>
+    </div> {# /.col- #}
+
+  </div>
+
+  <div class="row page-list m-t-30">
+    <div class="col-md-12">
+      {% include '../widget/page_list_and_timeline.html' %}
+    </div>
+  </div>
+{% endblock %}
+
+
+{% block content_footer %}
+  {% if page %}
+    {% include '../widget/page_attachments.html' %}
+  {% endif %}
+{% endblock %}
+
+
+{% block body_end %}
+  <div id="presentation-layer" class="fullscreen-layer">
+    <div id="presentation-container"></div>
+  </div>
+  <div id="crowi-modals">
+    {% include '../widget/page_modals.html' %}
+    {% include '../modal/what_is_portal.html' %}
+    {% include '../modal/unportalize.html' %}
+  </div>
+{% endblock %}

+ 67 - 0
lib/views/layout-growi/user_page.html

@@ -0,0 +1,67 @@
+{% extends 'page.html' %}
+
+{% block main_css_class %}
+  {% parent %}
+  user-page
+{% endblock %}
+
+{% block content_header %}
+  {% if pageUser %}
+    {% include '../widget/user_page_header.html' %}
+  {% else %}
+    {% parent %}
+  {% endif %}
+{% endblock %}
+
+
+{% block content_main %}
+  <div class="row">
+
+    <div class="col-lg-10 col-md-9">
+
+      {#
+        # ensure to insert 'user_page_content' widget to here
+        #
+        #   Because this block has content like 'Bookmarks' or 'Recent Created' whose height changes dynamically,
+        #   setting of 'revision-toc' (affix) is hindered.
+        #}
+      <div class="m-b-30 user-page-content-container">
+        {% include '../widget/user_page_content.html' %}
+      </div>
+
+      {% block content_main_before %}
+        {% parent %}
+      {% endblock %}
+
+      {% include '../widget/page_content.html' %}
+
+      {# force remove #revision-toc from #content_main of parent #}
+      <script>
+        $('#revision-toc').remove();
+      </script>
+
+    </div> {# /.col- #}
+
+    {# relocate #revision-toc #}
+    <div class="col-lg-2 col-md-3 revision-toc-container hidden-sm hidden-xs">
+      <div id="revision-toc" class="revision-toc" data-spy="affix" data-offset-top="75">
+        <div id="revision-toc-content" class="revision-toc-content"></div>
+      </div>
+    </div> {# /.col- #}
+
+  </div>
+
+  {% if 'growi' === behaviorType() || 'crowi-plus' === behaviorType() %}
+  <div class="row page-list m-t-30">
+    <div class="col-md-12">
+      {% include '../widget/page_list_and_timeline.html' %}
+    </div>
+  </div>
+  {% endif %}
+
+{% endblock %}
+
+
+{% block content_main_after %}
+  {% include 'widget/comments.html' %}
+{% endblock %}

+ 5 - 5
lib/views/crowi-plus/widget/comments.html → lib/views/layout-growi/widget/comments.html

@@ -2,17 +2,17 @@
 
   <div class="page-comments col-lg-7 col-md-9">
 
-    <h4><i class="fa fa-comments"></i> Comments</h4>
+    <h4><i class="icon-fw icon-bubbles"></i> Comments</h4>
 
     <div class="page-comments-list" id="page-comments-list">
       {# transplanted to PageComments React component -- 2017.06.02 Yuki Takei
       <div class="page-comments-list-newer collapse" id="page-comments-list-newer"></div>
 
-      <a class="page-comments-list-toggle-newer text-center" data-toggle="collapse" href="#page-comments-list-newer"><i class="fa fa-angle-double-up"></i> Comments for Newer Revision <i class="fa fa-angle-double-up"></i></a>
+      <a class="page-comments-list-toggle-newer text-center" data-toggle="collapse" href="#page-comments-list-newer"><i class="ti-angle-double-up"></i> Comments for Newer Revision <i class="ti-angle-double-up"></i></a>
 
       <div class="page-comments-list-current" id="page-comments-list-current"></div>
 
-      <a class="page-comments-list-toggle-older text-center" data-toggle="collapse" href="#page-comments-list-older"><i class="fa fa-angle-double-down"></i> Comments for Older Revision <i class="fa fa-angle-double-down"></i></a>
+      <a class="page-comments-list-toggle-older text-center" data-toggle="collapse" href="#page-comments-list-older"><i class="ti-angle-double-down"></i> Comments for Older Revision <i class="ti-angle-double-down"></i></a>
 
       <div class="page-comments-list-older collapse in" id="page-comments-list-older"></div>
       #}
@@ -22,7 +22,7 @@
     <form class="form page-comment-form" id="page-comment-form" onsubmit="return false;">
       <div class="comment-form">
         <div class="comment-form-user">
-            <img src="{{ user|picture }}" class="picture picture-rounded" width="25" alt="{{ user.name }}" title="{{ user.name }}" />
+            <img src="{{ user|picture }}" class="picture img-circle" width="25" alt="{{ user.name }}" title="{{ user.name }}" />
         </div>
         <div class="comment-form-main">
           <div class="comment-write" id="comment-write">
@@ -35,7 +35,7 @@
             <input type="hidden" name="commentForm[revision_id]" value="{{ revision._id.toString() }}">
             <div class="pull-right">
               <span class="text-danger" id="comment-form-message"></span>
-              <button type="submit" id="comment-form-button" class="btn btn-primary form-inline" {% if not user %}disabled{% endif %}>
+              <button type="submit" id="comment-form-button" class="fcbtn btn btn-sm btn-outline btn-rounded btn-primary btn-1b" {% if not user %}disabled{% endif %}>
                 Comment
               </button>
             </div>

+ 51 - 0
lib/views/layout-growi/widget/header.html

@@ -0,0 +1,51 @@
+<div class="header-wrap">
+  <header id="page-header">
+    <div class="d-flex align-items-center">
+      <div class="title-logo-container hidden-xs hidden-sm">
+        <a class="logo" href="/">
+          <div class="logo-mark">{% include '../../widget/logo.html' %}</div>
+        </a>
+      </div>
+      <div class="title-container">
+        <h1 class="title flex-item-title" id="revision-path"></h1>
+        <div id="revision-url" class="url-line"></div>
+      </div>
+      {% if page %}
+      {% include '../../widget/header-buttons.html' %}
+
+      <ul class="authors hidden-sm hidden-xs">
+        <li>
+          <div class="d-flex align-items-center">
+            <a class="m-r-5" href="{{ userPageRoot(page.creator) }}">
+              <img src="{{ page.creator|default(author)|picture }}" class="picture img-circle">
+            </a>
+            <div>
+              <div>Created by <a href="{{ userPageRoot(page.creator) }}">{{ page.creator.name|default(author.name) }}</a></div>
+              <div class="text-muted">{{ page.createdAt|datetz('Y/m/d H:i:s') }}</div>
+            </div>
+          </div>
+        </li>
+        <li class="m-t-5">
+          <div class="d-flex align-items-center">
+            <a class="m-r-5" href="{{ userPageRoot(page.lastUpdateUser) }}">
+              <img src="{{ page.lastUpdateUser|default(author)|picture }}" class="picture img-circle">
+            </a>
+            <div>
+              <div>Updated by <a href="{{ userPageRoot(page.lastUpdateUser) }}">{{ page.lastUpdateUser.name|default(author.name) }}</a></div>
+              <div class="text-muted">{{ page.updatedAt|datetz('Y/m/d H:i:s') }}</div>
+            </div>
+          </div>
+        </li>
+      </ul>
+      {% endif %}
+
+      {% if not page and ('/' === path or 'crowi' === behaviorType()) and not isUserPageList(path) and !isTrashPage() %}
+        {% if '/' === path.slice(-1) %}
+          {% include '../../widget/create_portal.html' %}
+        {% endif %}
+      {% endif %}
+
+    </div>
+
+  </header>
+</div>

+ 0 - 57
lib/views/layout/2column.html

@@ -1,57 +0,0 @@
-{% extends 'layout.html' %}
-
-{% block layout_sidebar %}
-
-<a href="" class=" hidden-xs hidden-sm layout-control" id="toggle-sidebar"><i class="fa fa-chevron-right"></i> <span class="hide-on-affix-top"></span></a>
-<aside class="sidebar col-md-3 hidden-xs hidden-sm hidden-print">
-
-  {% block side_header %}
-  {% endblock %}
-
-  <div class="side-content">
-    {% block side_content %}
-    {% endblock %}
-  </div>
-
-  {% block side_footer %}
-  {% endblock %}
-
-  <div id="footer-container" class="footer">
-    <footer class="">
-      <p>
-        <a href="https://github.com/weseek/crowi-plus">crowi-plus</a> {{ crowiVersion() }}
-        <a href="" class="pull-right" data-target="#shortcuts-modal" data-toggle="modal"><i class="fa fa-keyboard-o"></i>&nbsp;<span class="cmd-key"></span>-/</a>
-      </p>
-    </footer>
-  </div>
-</aside>
-
-{% endblock %} {# layout_sidebar #}
-
-{% block layout_main %}
-<div id="main" class="main col-md-9 {% if page %}{{ css.grant(page) }}{% endif %} {% block main_css_class %}{% endblock %}">
-  {% if page && page.grant != 1 %}
-  <p class="page-grant">
-    <i class="fa fa-lock"></i> {{ consts.pageGrants[page.grant] }} ({{ t('Browsing of this page is restricted') }})
-  </p>
-  {% endif %}
-  {% if page && page.grant == 2 %}
-  <p class="alert alert-info">
-    {{ t('Shareable Link') }}
-    <input type="text" class="copy-link form-control" value="{{ baseUrl }}/{{ page._id.toString() }}" readonly>
-  </p>
-  {% endif %}
-  <article>
-    {% block content_head %}
-    {% endblock %}
-
-    {% block content_main %}
-    //
-    {% endblock content_main %}
-
-    {% block content_footer %}
-    {% endblock %}
-  </article>
-</div>
-
-{% endblock %} {# layout_main #}

+ 5 - 1
lib/views/layout/admin.html

@@ -1,4 +1,8 @@
-{% extends 'single.html' %}
+{% extends '../layout-growi/base/layout.html' %}
+
+
+{% block main_css_class %}admin-page{% endblock %}
+
 
 {% block html_additional_headers %}
   {% parent %}

+ 128 - 112
lib/views/layout/layout.html

@@ -4,23 +4,15 @@
 <head>
   <meta charset="utf-8">
   <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
-
-  <title>{% block html_title %}{% endblock %} {{ config.crowi['app:title']|default('Crowi') }}</title>
+  <title>{% block html_title %}{{ path|path2name }} · {{ path }}{% endblock %} {{ appTitle() }}</title>
   <meta name="description" content="">
   <meta name="author" content="">
 
   <meta name="viewport" content="width=device-width,initial-scale=1">
 
-  <meta name="apple-mobile-web-app-title" content="{{ config.crowi['app:title']|default('Crowi') }}">
+  <meta name="apple-mobile-web-app-title" content="{{ appTitle() }}">
 
-  <link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
-  <link rel="apple-touch-icon"                 href="/apple-touch-icon.png">
-  <link rel="apple-touch-icon" sizes="72x72"   href="/apple-touch-icon-72x72.png">
-  <link rel="apple-touch-icon" sizes="120x120" href="/apple-touch-icon-120x120.png">
-  <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon-180x180.png">
-  <link rel="icon" type="image/png" href="/favicon-32x32.png" sizes="32x32">
-  <link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96">
-  <link rel="icon" type="image/png" href="/android-chrome-192x192.png" sizes="192x192">
+  {% include '../widget/favicon.html' %}
 
   {{ customHeader() }}
 
@@ -72,22 +64,37 @@ gh/highlightjs/cdn-release@9.12.0/build/languages/yaml.min.js
   {% if env === 'development' %}
     <script src="/dll/vendor.dll.js"></script>
     <script src="{{ webpack_asset('dev').js }}" async></script>
+    <!-- Browsersync -->
+    <script id="__bs_script__">//<![CDATA[
+      document.write("<script async src='http://HOST:3001/browser-sync/browser-sync-client.js?v=2.23.6'><\/script>".replace("HOST", location.hostname));
+    //]]></script>
   {% endif %}
 
-  <script src="{{ webpack_asset('style').js }}"></script>
   <script src="{{ webpack_asset('commons').js }}" defer></script>
   {% if isEnabledPlugins() %}
-    <script src="{{ webpack_asset('plugin').js }}" defer></script>
+  <script src="{{ webpack_asset('plugin').js }}" defer></script>
   {% endif %}
   {% block html_head_loading_legacy %}
-    <script src="{{ webpack_asset('legacy').js }}" defer></script>
+  <script src="{{ webpack_asset('legacy').js }}" defer></script>
   {% endblock %}
+  {% block html_head_loading_app %}
   <script src="{{ webpack_asset('app').js }}" defer></script>
+  {% endblock %}
+
+  <!-- styles -->
+  {% block style_css_block %}
+  <link rel="stylesheet" href="{{ webpack_asset('style').css }}">
+  <link rel="stylesheet" href="{{ webpack_asset('style-theme-' + theme()).css }}">
+  {% endblock %}
 
   <!-- Google Fonts -->
   <link href='https://fonts.googleapis.com/css?family=Lato:400,700' rel='stylesheet' type='text/css'>
   <!-- Font Awesome -->
   <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css">
+  <!-- Themify Icons -->
+  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/cd-themify-icons@0.0.1/index.min.css">
+  <!-- Simple Line icons -->
+  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/simple-line-icons@2.4.1/css/simple-line-icons.min.css">
   <!-- emojione -->
   <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/emojione@3.1.2/extras/css/emojione.min.css">
   <!-- highlight.js -->
@@ -103,117 +110,126 @@ gh/highlightjs/cdn-release@9.12.0/build/languages/yaml.min.js
 
 {% block html_body %}
 <body
-  class="crowi main-container {% block html_base_css %}{% endblock %} {% if 'crowi-plus' === layoutType() %}crowi-plus{% endif %}"
+  class="main-container content-wrapper {% block html_base_css %}{% endblock %}
+      {% if !layoutType() || 'crowi' === layoutType() %}crowi{% else %}growi{% endif %}"
   data-me="{{ user._id.toString() }}"
   data-plugin-enabled="{{ isEnabledPlugins() }}"
- {% block html_base_attr %}{% endblock %}
+  {% block html_base_attr %}{% endblock %}
   data-csrftoken="{{ csrf() }}"
   data-current-username="{% if user %}{{ user.username }}{% endif %}"
  >
 
-{% block layout_head_nav %}
-<nav class="crowi-header navbar navbar-default" role="navigation">
-  <!-- Brand and toggle get grouped for better mobile display -->
-  <div class="navbar-header">
-    <a class="navbar-brand" href="/">
-      <img alt="Crowi" src="/logo/32x32.png" width="16">
-      <span class="hidden-xs">{% block title %}{{ config.crowi['app:title']|default('Crowi') }}{% endblock %}</span>
-    </a>
-  {% if searchConfigured() %}
-  <div class="navbar-form navbar-left search-top" role="search" id="search-top">
-  </div>
-  {% endif %}
+<div id="wrapper">
+  <!-- Navigation -->
+  {% block layout_head_nav %}
+  <nav class="navbar navbar-default navbar-static-top m-b-0">
+    <div class="navbar-header">
+      <a class="navbar-toggle hidden-sm hidden-md hidden-lg " href="javascript:void(0)" data-toggle="collapse" data-target=".navbar-collapse">
+        <i class="ti-menu"></i>
+      </a>
+      <div class="top-left-part">
+        <a class="logo" href="/">
+          <b>
+            <div class="logo-mark">{% include '../widget/logo.html' %}</div>
+          </b>
+          <span class="hidden-xs" style="color: black">
+            {% set appTitle = appTitle() %}
+            {% set appTitleFontSize = getAppTitleFontSize(appTitle) %}
+            <span class="logo-text">
+              <svg xmlns="http://www.w3.org/2000/svg">
+                <text x="0" y="{{22+appTitleFontSize/2}}" font-size="{{appTitleFontSize}}">
+                  {% block title %}{{ appTitle }}{% endblock %}
+                </text>
+              </svg>
+            </span>
+          </span>
+        </a>
+      </div>
+
+      <ul class="nav navbar-top-links navbar-left hidden-xs">
+        <li>
+          <a class="open-close hidden-xs waves-effect waves-light">
+            <i class="ti-menu"></i>
+          </a>
+        </li>
+        <li>
+          {% if searchConfigured() %}
+          <div class="navbar-form navbar-left search-top" role="search" id="search-top"></div>
+          {% endif %}
+        </li>
+      </ul>
+
+      <ul class="nav navbar-top-links navbar-right pull-right">
+        {% if user and user.admin %}
+        <li id="">
+          <a href="/admin" id="link-mypage">
+            <i class="icon-settings"></i> {{ t('Admin') }}
+          </a>
+        </li>
+        {% endif %}
+
+        {% if user %}
+        <li id="" class="dropdown">
+          <a href="#" data-target="#create-page" data-toggle="modal">
+            <i class="icon-pencil"></i> {{ t('New') }}
+          </a>
+        </li>
+        <li class="dropdown">
+          <a class="dropdown-toggle waves-effect waves-light" data-toggle="dropdown">
+            <img src="{{ user|picture }}" class="picture img-circle" width="25" /> {{ user.name }}
+          </a>
+          <ul class="dropdown-menu">
+            <li><a href="/user/{{ user.username }}"><i class="icon-fw icon-home"></i>{{ t('Home') }}</a></li>
+            <li><a href="/me"><i class="icon-fw icon-wrench"></i>{{ t('User Settings') }}</a></li>
+            <li role="separator" class="divider"></li>
+            <li><a href="/trash"><i class="icon-fw icon-trash"></i>{{ t('Deleted Pages') }}</a></li>
+            <li role="separator" class="divider"></li>
+            <li><a href="/logout"><i class="icon-fw icon-power"></i>{{ t('Sign out') }}</a></li>
+          </ul>
+          <!-- /.dropdown-messages -->
+        </li>
+        {% else %}
+        <li id="login-user"><a href="/login">Login</a></li>
+        {% endif %}
+        {% if config.crowi['app:confidential'] && config.crowi['app:confidential'] != '' %}
+        <li class="confidential"><a href="#">{{ config.crowi['app:confidential'] }}</a></li>
+        {% endif %}
+      </ul>
+    </div><!-- /.navbar-header -->
+  </nav>
+  {% include '../modal/create_page.html' %}
+  {% endblock  %} {# layout_head_nav #}
+
+  {% block sidebar %}
+  <!-- Left navbar-header -->
+  <div class="navbar-default sidebar" role="navigation">
+    <div class="sidebar-nav navbar-collapse slimscrollsidebar">
+      <ul class="nav" id="side-menu">
+        <li class="sidebar-search hidden-sm hidden-md hidden-lg">
+          {% if searchConfigured() %}
+          <div class="search-sidebar" role="search" id="search-sidebar"></div>
+          {% endif %}
+        </li>
+
+        <li><a href="#">(TBD) Create /Sidebar</a></li>
+      </ul>
+    </div>
   </div>
+  <!-- Left navbar-header end -->
+  {% endblock %}
 
+  <!-- Page Content -->
+  <div id="page-wrapper">
+    {% block layout_main %}
+    {% endblock %} {# layout_main #}
+  </div><!-- /#page-wrapper -->
 
-  <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#navbarCollapse">
-    <span class="sr-only">Toggle navigation</span>
-    <span class="icon-bar"></span>
-    <span class="icon-bar"></span>
-    <span class="icon-bar"></span>
-  </button>
-  <!-- Collect the nav links, forms, and other content for toggling -->
-  <div class="collapse navbar-collapse" id="navbarCollapse">
-
-    <ul class="nav navbar-nav navbar-right">
-
-      {% if user and user.admin %}
-      <li id="">
-        <a href="/admin" id="link-mypage">
-          <i class="fa fa-cube"></i> {{ t('Admin') }}
-        </a>
-      </li>
-      {% endif %}
-      {#
-      <li id="">
-        <a href="#" id="createPage">
-          <i class="fa fa-plus"> 新規</i>
-        </a>
-      </li>
-      #}
-      {% if user %}
-      {#
-      <li id="" class="notif">
-        <a href="" id="notif-opener">
-          <i class="fa fa-globe"></i> <span class="badge badge-danger">6</span>
-        </a>
-      </li>
-      #}
-      <li id="" class="dropdown">
-        <button class="btn btn-default create-page-button" data-target="#create-page" data-toggle="modal">
-          <i class="fa fa-pencil"></i> {{ t('New') }}
-        </button>
-      </li>
-      <li id="login-user">
-        <a href="/user/{{ user.username }}" id="link-mypage">
-          <img src="{{ user|picture }}" class="picture picture-rounded" width="25" /> {{ user.name }}
-        </a>
-      </li>
-      <li class="dropdown">
-        <a href="#" class="dropdown-toggle" data-toggle="dropdown"><i class="fa fa-bars"></i> <label class="sr-only">メニュー</label></a>
-        <ul class="dropdown-menu">
-          <li><a href="/me"><i class="fa fa-gears"></i> {{ t('User Settings') }}</a></li>
-          <li class="divider"></li>
-          <li><a href="/trash/"><i class="fa fa-trash-o"></i> {{ t('Deleted Pages') }}</a></li>
-          <li class="divider"></li>
-          <li><a href="/logout"><i class="fa fa-sign-out"></i> {{ t('Sign out') }}</a></li>
-          {# <li><a href="#">今日の日報を作成</a></li> #}
-          {# <li class="divider"></li> #}
-          {# <li class="divider"></li> #}
-          {# <li><a href="#">ログアウト</a></li> #}
-        </ul>
-      </li>
-      {% else %}
-      <li id="login-user"><a href="/login"><i class="fa fa-user"></i> Login</a></li>
-      {% endif %}
-      {% if config.crowi['app:confidential'] && config.crowi['app:confidential'] != '' %}
-      <li class="confidential"><a href="#">{{ config.crowi['app:confidential'] }}</a></li>
-      {% endif %}
-    </ul>
-  </div><!-- /.navbar-collapse -->
-</nav>
-{% include '../modal/create_page.html' %}
-{% endblock  %} {# layout_head_nav #}
-
-<div class="container-fluid">
-  <div class="row">
-
-  {% block layout_sidebar %}
-  {% endblock %} {# layout_sidebar #}
-
-  {% block layout_main %}
-  {% endblock %} {# layout_main #}
-
-{% block footer %}
-{% endblock %}
+</div><!-- /#wrapper -->
 
-  </div> {# /.row #}
-</div> {# /.container-fluid #}
+{% include '../modal/shortcuts.html' %}
 
 {% block body_end %}
 {% endblock %}
-
-{% include '../modal/shortcuts.html' %}
 </body>
 {% endblock %}
 

+ 0 - 22
lib/views/layout/single-nologin.html

@@ -1,22 +0,0 @@
-{% extends 'layout.html' %}
-
-{% block html_base_css %}single nologin{% endblock %}
-
-{% block layout_head_nav %}
-{% endblock  %} {# layout_head_nav #}
-
-{% block layout_sidebar %}
-{% endblock  %} {# layout_sidebar  #}
-
-{% block layout_main %}
-
-  {% block content_head %}
-  {% endblock %}
-
-  {% block content_main %}
-  {% endblock content_main %}
-
-  {% block content_footer %}
-  {% endblock %}
-
-{% endblock %} {# layout_main #}

+ 0 - 45
lib/views/layout/single.html

@@ -1,45 +0,0 @@
-{% extends 'layout.html' %}
-
-{% block layout_main %}
-<div id="main" class="main col-md-12">
-  <article>
-    {% block content_head %}
-    {% endblock %}
-
-    {% block content_main %}
-    {% endblock content_main %}
-
-    {% block content_footer %}
-    {% endblock %}
-  </article>
-</div>
-
-{% endblock %} {# layout_main #}
-
-{% block footer %}
-{% parent %}
-<div class="system-version">
-  <span>
-    <a href="https://github.com/weseek/crowi-plus">crowi-plus</a> {{ crowiVersion() }}
-  </span>
-  <span>
-    <a href="" data-target="#shortcuts-modal" data-toggle="modal"><i class="fa fa-keyboard-o"></i>&nbsp;<span class="cmd-key"></span>-/</a>
-  </span>
-</div>
-<script>
-  /*
-  * add classes to cmd-key by OS
-  */
-  var platform = navigator.platform.toLowerCase();
-  var isMac = (platform.indexOf('mac') > -1);
-
-  document.querySelectorAll('.system-version .cmd-key').forEach((element) => {
-    if (isMac) {
-      element.classList.add('mac');
-    }
-    else {
-      element.classList.add('win', 'key-longer');
-    }
-  })
-</script>
-{% endblock %}

+ 250 - 205
lib/views/login.html

@@ -1,241 +1,286 @@
-{% extends 'layout/single-nologin.html' %}
+{% extends 'layout/layout.html' %}
+
+{% block html_base_css %}login-page nologin{% endblock %}
 
 {% block html_title %}{{ t('Sign in') }} · {% endblock %}
 
-{% block content_main %}
 
-<h1 class="login-page">
-  {% if config.crowi['app:title'] == 'Crowi' %}
-    <img src="/logo/135x32.png" alt="Crowi">
-  {% else %}
-    {{ config.crowi['app:title'] }}<br>
-    <img src="/logo/100x11_w.png" alt="powered by Crowi">
-  {% endif %}
-</h1>
 
-<div class="login-dialog-container flip-container col-md-5">
+{#
+ # Remove default contents
+ #}
+{% block html_head_loading_legacy %}
+{% endblock %}
+{% block html_head_loading_app %}
+{% endblock %}
+{% block layout_head_nav %}
+{% endblock %}
+{% block sidebar %}
+{% endblock %}
 
-<div class="login-dialog flipper {% if req.query.register or req.body.registerForm or isRegistering or googleId %}to-flip{% endif %}" id="login-dialog">
 
-  <div class="login-dialog-inner front">
-    <h2>{{ t('Sign in') }}</h2>
 
-    <div id="login-form-errors">
-      {% if isLdapSetupFailed() %}
-      <div class="alert alert-warning">
-        LDAP is enabled but the configuration has something wrong.<br>
-        <small>(set the environment variables <code>DEBUG=crowi:service:PassportService</code> and get the logs)</small>
-      </div>
-      {% endif %}
-
-      {#
-       # The case that there already exists a user whose username matches ID of the newly created LDAP user
-       # https://github.com/weseek/crowi-plus/issues/193
-       #}
-      {% set isDuplicatedUsernameExceptionOccured = req.flash('isDuplicatedUsernameExceptionOccured') %}
-      {% if isDuplicatedUsernameExceptionOccured != null %}
-      <div class="alert alert-warning">
-        <i class="fa fa-fw fa-info-circle"></i>
-        <strong>DuplicatedUsernameException occured</strong>
-        <p>
-          Your LDAP authentication was succeess, but a new user could not be created.
-          See the issue <a href="https://github.com/weseek/crowi-plus/issues/193">#193</a>.
-        </p>
-      </div>
-      {% endif %}
+{% block layout_main %}
 
-      {% set success = req.flash('successMessage') %}
-      {% if success.length %}
-      <div class="alert alert-success">
-        {{ success }}
-      </div>
-      {% endif %}
+<div class="main container-fluid">
 
-      {% set warn = req.flash('warningMessage') %}
-      {% if warn.length %}
-      {% for w in warn %}
-      <div class="alert alert-warning">
-        {{ w }}
-      </div>
-      {% endfor %}
-      {% endif %}
-
-      {% set error = req.flash('errorMessage') %}
-      {% if error.length %}
-      {% for e in error %}
-      <div class="alert alert-danger">
-        {{ e }}
-      </div>
-      {% endfor %}
-      {% endif %}
-
-      {% if req.form.errors.length > 0 %}
-      <div class="alert alert-danger">
-        <ul>
-        {% for error in req.form.errors %}
-          <li>{{ error }}</li>
-        {% endfor %}
-        </ul>
-      </div>
-      {% endif %}
-    </div>
-    <form role="form" action="/login" method="post">
-      <div class="input-group">
-        <span class="input-group-addon"><i class="fa fa-fw fa-user"></i></span>
-        <input type="text" class="form-control" placeholder="Username or E-mail" name="loginForm[username]">
-        {% if isLdapSetup() %}
-        <span class="input-group-addon">
-          <small class="text-primary">
-            <i class="fa fa-fw fa-check-circle"></i> LDAP
-          </small>
-        </span>
+  <div class="row">
+    <div class="login-header col-sm-offset-4 col-sm-4">
+      <div class="logo">{% include 'widget/logo.html' %}</div>
+      <h1>{{ appTitle() }}</h1>
+
+      <div class="login-form-errors">
+        {% if isLdapSetupFailed() %}
+        <div class="alert alert-warning small">
+          <strong><i class="icon-fw icon-info"></i>LDAP is enabled but the configuration has something wrong.</strong>
+          <br>
+          (Please set the environment variables <code>DEBUG=crowi:service:PassportService</code> to get the logs)
+        </div>
         {% endif %}
-      </div>
 
-      <div class="input-group">
-        <span class="input-group-addon"><i class="fa fa-fw fa-key"></i></span>
-        <input type="password" class="form-control" placeholder="Password" name="loginForm[password]">
-      </div>
+        {#
+        # The case that there already exists a user whose username matches ID of the newly created LDAP user
+        # https://github.com/weseek/growi/issues/193
+        #}
+        {% set isDuplicatedUsernameExceptionOccured = req.flash('isDuplicatedUsernameExceptionOccured') %}
+        {% if isDuplicatedUsernameExceptionOccured != null %}
+        <div class="alert alert-warning small">
+          <p><strong><i class="icon-fw icon-ban"></i>DuplicatedUsernameException occured</strong></p>
+          <p>
+            Your LDAP authentication was succeess, but a new user could not be created.
+            See the issue <a href="https://github.com/weseek/growi/issues/193">#193</a>.
+          </p>
+        </div>
+        {% endif %}
 
-      <input type="hidden" name="_csrf" value="{{ csrf() }}">
-      <input type="submit" class="btn btn-primary btn-lg btn-block" value="{{ t('Sign in') }}">
-    </form>
+        {% set success = req.flash('successMessage') %}
+        {% if success.length %}
+        <div class="alert alert-success">
+          {{ success }}
+        </div>
+        {% endif %}
 
-    <hr>
+        {% set warn = req.flash('warningMessage') %}
+        {% if warn.length %}
+        {% for w in warn %}
+        <div class="alert alert-warning">
+          {{ w }}
+        </div>
+        {% endfor %}
+        {% endif %}
 
-    <div class="row">
-      {% if googleLoginEnabled() %}
-      <div class="col-md-8">
-        <p>{{ t('Sign in by Google Account') }}</p>
-        <form role="form" action="/login/google" method="get">
-          <button type="submit" class="btn btn-block btn-google"><i class="fa fa-google-plus-square"></i> {{ t('Sign in') }}</button>
-          <input type="hidden" name="_csrf" value="{{ csrf() }}">
-        </form>
+        {% set error = req.flash('errorMessage') %}
+        {% if error.length %}
+        {% for e in error %}
+        <div class="alert alert-danger">
+          {{ e }}
+        </div>
+        {% endfor %}
+        {% endif %}
+
+        {% if req.form.errors.length > 0 %}
+        <div class="alert alert-danger">
+          <ul>
+          {% for error in req.form.errors %}
+            <li>{{ error }}</li>
+          {% endfor %}
+          </ul>
+        </div>
+        {% endif %}
+      </div>
+      <div id="register-form-errors">
+        {% set message = req.flash('registerWarningMessage') %}
+        {% if message.length %}
+        <div class="alert alert-danger">
+          {% for msg in message %}
+          {{ msg }}<br>
+          {% endfor  %}
+        </div>
+        {% endif %}
       </div>
-      {% endif %}
     </div>
 
-    {% if config.crowi['security:registrationMode'] != 'Closed' %}
-    <p class="bottom-text"><a href="#register" id="register"><i class="fa fa-pencil"></i> {{ t('Sign up is here') }}</a></p>
-    {% endif %}
-  </div>
+    <div class="login-dialog p-b-10 col-sm-offset-4 col-sm-4 flipper {% if req.query.register or req.body.registerForm or isRegistering or googleId %}to-flip{% endif %}" id="login-dialog">
+
+      <div class="front">
+        <form role="form" action="/login" method="post">
+          <div class="input-group">
+            <span class="input-group-addon"><i class="icon-user"></i></span>
+            <input type="text" class="form-control" placeholder="Username or E-mail" name="loginForm[username]">
+            {% if isLdapSetup() %}
+            <span class="input-group-addon">
+              <small class="text-success">
+                <i class="icon-fw icon-check"></i> LDAP
+              </small>
+            </span>
+            {% endif %}
+          </div>
+
+          <div class="input-group">
+            <span class="input-group-addon"><i class="icon-lock"></i></span>
+            <input type="password" class="form-control" placeholder="Password" name="loginForm[password]">
+          </div>
+
+          <div class="input-group m-t-30 m-b-20 d-flex justify-content-center">
+            <input type="hidden" name="_csrf" value="{{ csrf() }}">
+            <button type="submit" class="fcbtn btn btn-danger btn-1b btn-login">
+              <span class="btn-label"><i class="icon-login"></i></span>
+              {{ t('Sign in') }}
+            </button>
+          </div>
+        </form>
 
-  {% if config.crowi['security:registrationMode'] != 'Closed' %}
-  <div class="register-dialog-inner back">
-
-    <h2>{{ t('Sign up') }}</h2>
-
-    {% if config.crowi['security:registrationMode'] == 'Restricted' %}
-    <p class="alert alert-warning">
-      {{ t('page_register.notice.restricted') }}<br>
-      {{ t('page_register.notice.restricted_defail') }}
-    </p>
-    {% endif %}
-
-    {% if googleId %}
-    <div class="google-info alert alert-info">
-      {% if googleImage %}
-      <p class="text-center">
-        <img src="{{ googleImage }}" class="picture picture-rounded picture-lg">
-      </p>
-      {% endif %}
-      <code>{{ googleEmail }}</code> {{ t('page_register with this Google Account') }}<br>
-      {{ t('page_register.notice.google_account_continue') }}
-    </div>
-    {% endif %}
-
-    <div id="register-form-errors">
-      {% set message = req.flash('registerWarningMessage') %}
-      {% if message.length %}
-      <div class="alert alert-danger">
-        {% for msg in message %}
-        {{ msg }}<br>
-        {% endfor  %}
-      </div>
-      {% endif %}
+        {% if googleLoginEnabled() %}
+        <hr>
+
+        <div class="input-group m-t-15 m-b-10 mx-auto">
+          <form role="form" action="/login/google" method="get">
+            <input type="hidden" name="_csrf" value="{{ csrf() }}">
+            <button type="submit" class="fcbtn btn btn-danger btn-1b btn-login-google">
+              <span class="btn-label"><i class="icon-social-google"></i></span>
+              {{ t('Sign in') }}
+            </button>
+            <div class="small text-right">by Google Account</div>
+          </form>
+        </div>
+        {% endif %}
 
-      {% if req.form.errors.length > 0 %}
-      <div class="alert alert-danger">
-        <ul>
-        {% for error in req.form.errors %}
-          <li>{{ error }}</li>
-        {% endfor %}
-        </ul>
+        <hr>
+
+        {% if config.crowi['security:registrationMode'] != 'Closed' %}
+        <div class="row">
+          <div class="col-xs-12 text-right">
+            <a href="#register" id="register" class="link-switch">
+              <i class="ti-check-box"></i> {{ t('Sign up is here') }}
+            </a>
+          </div>
+        </div>
+        {% endif %}
       </div>
-      {% endif %}
-    </div>
 
-    <form role="form" method="post" action="/register" id="register-form">
-      <input type="hidden" class="form-control" name="registerForm[googleId]" value="{{ googleId|default(req.body.registerForm.googleId) }}">
 
-      <label>{{ t('User ID') }}</label>
-      <div class="input-group" id="input-group-username">
-        <span class="input-group-addon"><strong>@</strong></span>
-        <input type="text" class="form-control" placeholder="{{ t('Example') }}: taroyama" name="registerForm[username]" value="{{ req.body.registerForm.username }}" required>
-      </div>
-      <p class="help-block">
-      <span id="help-block-username" class="text-danger"></span>
-      {{ t('page_register.form_help.user_id') }}
-      </p>
-
-      <label>{{ t('Name') }}</label>
-      <div class="input-group">
-        <span class="input-group-addon"><i class="fa fa-user"></i></span>
-        <input type="text" class="form-control" placeholder="{{ t('Example') }}: {{ t('Taro Yamada') }}" name="registerForm[name]" value="{{ googleName|default(req.body.registerForm.name) }}" required>
-      </div>
+      {% if config.crowi['security:registrationMode'] != 'Closed' %}
+      <div class="back">
+        {% if config.crowi['security:registrationMode'] == 'Restricted' %}
+        <p class="alert alert-warning">
+          {{ t('page_register.notice.restricted') }}<br>
+          {{ t('page_register.notice.restricted_defail') }}
+        </p>
+        {% endif %}
 
-      <label>{{ t('Email') }}</label>
-      <div class="input-group">
-        <span class="input-group-addon"><i class="fa fa-envelope"></i></span>
-        <input type="email" class="form-control" placeholder="E-mail" name="registerForm[email]" value="{{ googleEmail|default(req.body.registerForm.email) }}" required>
-      </div>
-      {% if config.crowi['security:registrationWhiteList'] && config.crowi['security:registrationWhiteList'].length %}
-      <p class="help-block">
-        {{ t('page_register.form_help.email') }}
-      </p>
-      <ul>
-        {% for em in config.crowi['security:registrationWhiteList'] %}
-        <li><code>{{ em }}</code></li>
-        {% endfor %}
-      </ul>
-      {% endif %}
+        {% if googleId %}
+        <div class="google-info alert alert-info">
+          {% if googleImage %}
+          <p class="text-center">
+            <img src="{{ googleImage }}" class="img-circle img-circle-lg">
+          </p>
+          {% endif %}
+          <code>{{ googleEmail }}</code> {{ t('page_register with this Google Account') }}<br>
+          {{ t('page_register.notice.google_account_continue') }}
+        </div>
+        {% endif %}
 
-      <label>{{ t('Password') }}</label>
-      <div class="input-group">
-        <span class="input-group-addon"><i class="fa fa-key"></i></span>
-        <input type="password" class="form-control" placeholder="Password" name="registerForm[password]" required>
-      </div>
-      <p class="help-block">
-        {{ t('page_register.form_help.password') }}
-      </p>
-
-      {% if googleImage %}
-        <input type="hidden" name="registerForm[googleImage]" value="{{ googleImage }}">
-      {% endif  %}
-      <input type="hidden" name="_csrf" value="{{ csrf() }}">
-      <input type="submit" class="btn btn-primary btn-lg btn-block" value="{{ t('Sign up') }}">
-    </form>
-
-    <hr>
-
-    <div class="row">
-      {% if googleLoginEnabled() %}
-      <div class="col-md-6">
-        <p>{{ t('Sign up with Google Account') }}</p>
-        <form role="form" method="post" action="/register/google">
+        <form role="form" method="post" action="/register" id="register-form">
+          <input type="hidden" class="form-control" name="registerForm[googleId]" value="{{ googleId|default(req.body.registerForm.googleId) }}">
+
+          <div class="input-group" id="input-group-username">
+            <span class="input-group-addon"><i class="icon-user"></i></span>
+            <input type="text" class="form-control" placeholder="{{ t('User ID') }}" name="registerForm[username]" value="{{ req.body.registerForm.username }}" required>
+          </div>
+          <p class="help-block">
+            <span id="help-block-username"></span>
+          </p>
+
+          <div class="input-group">
+            <span class="input-group-addon"><i class="icon-tag"></i></span>
+            <input type="text" class="form-control" placeholder="{{ t('Name') }}" name="registerForm[name]" value="{{ googleName|default(req.body.registerForm.name) }}" required>
+          </div>
+
+          <div class="input-group">
+            <span class="input-group-addon"><i class="icon-envelope"></i></span>
+            <input type="email" class="form-control" placeholder="{{ t('Email') }}" name="registerForm[email]" value="{{ googleEmail|default(req.body.registerForm.email) }}" required>
+          </div>
+          {% if config.crowi['security:registrationWhiteList'] && config.crowi['security:registrationWhiteList'].length %}
+          <p class="help-block">
+            {{ t('page_register.form_help.email') }}
+          </p>
+          <ul>
+            {% for em in config.crowi['security:registrationWhiteList'] %}
+            <li><code>{{ em }}</code></li>
+            {% endfor %}
+          </ul>
+          {% endif %}
+
+          <div class="input-group">
+            <span class="input-group-addon"><i class="icon-lock"></i></span>
+            <input type="password" class="form-control" placeholder="{{ t('Password') }}" name="registerForm[password]" required>
+          </div>
+
+          {% if googleImage %}
+            <input type="hidden" name="registerForm[googleImage]" value="{{ googleImage }}">
+          {% endif  %}
           <input type="hidden" name="_csrf" value="{{ csrf() }}">
-          <button type="submit" class="btn btn-block btn-google"><i class="fa fa-google-plus-square"></i> {{ t('Login') }}</button>
+
+          <div class="input-group m-t-30 m-b-20 d-flex justify-content-center">
+            <button type="submit" class="fcbtn btn btn-success btn-1b btn-register">
+              <span class="btn-label"><i class="icon-user-follow"></i></span>
+              {{ t('Sign up') }}
+            </button>
+          </div>
         </form>
+
+        <hr>
+
+        <div class="row">
+          <div class="col-xs-12 text-right">
+            <a href="#login" id="login" class="link-switch">
+              <i class="icon-fw icon-login"></i>{{ t('Sign in is here') }}
+            </a>
+          </div>
+        </div>
       </div>
-      {% endif %}
+      {% endif %} {# if registrationMode == Closed #}
+
+      <a href="https://growi.org" class="link-growi-org">
+        <span class="growi">GROWI</span>.<span class="org">ORG
+      </a>
+
     </div>
 
-    <p class="bottom-text"><a href="#login" id="login"><i class="fa fa-sign-out"></i> {{ t('Sign in is here') }}</a></p>
   </div>
-  {% endif %} {# if registrationMode == Closed #}
 
 </div>
 
-</div>
+{% endblock %}
+
 
+{% block body_end %}
+<script>
+  // login
+  $('#register').on('click', function() {
+    $('#login-dialog').addClass('to-flip');
+    return false;
+  });
+  $('#login').on('click', function() {
+    $('#login-dialog').removeClass('to-flip');
+    return false;
+  });
+
+  $('#register-form input[name="registerForm[username]"]').change(function(e) {
+    var username = $(this).val();
+    $('#login-dialog').removeClass('has-error');
+    $('#input-group-username').removeClass('has-error');
+    $('#help-block-username').html("");
+
+    $.getJSON('/_api/check_username', {username: username}, function(json) {
+      if (!json.valid) {
+        $('#help-block-username').html(
+          '<i class="icon-fw icon-ban"></i> This User ID is not available.'
+        );
+        $('#login-dialog').addClass('has-error');
+        $('#input-group-username').addClass('has-error');
+      }
+    });
+  });
+</script>
 {% endblock %}

+ 35 - 38
lib/views/login/error.html

@@ -1,56 +1,53 @@
-{% extends '../layout/single-nologin.html' %}
+{% extends '../layout/layout.html' %}
 
-{% block html_title %}Error · {% endblock %}
+{% block html_base_css %}error nologin{% endblock %}
 
-{% block content_main %}
+{% block html_title %}セットアップ {% endblock %}
 
-<h1 class="login-page">
-  {% if config.crowi['app:title'] == 'Crowi' %}
-    <img src="/logo/135x32.png" alt="Crowi">
-  {% else %}
-    {{ config.crowi['app:title'] }}<br>
-    <img src="/logo/100x11_w.png" alt="powered by Crowi">
-  {% endif %}
-</h1>
 
-<div class="login-dialog-container flip-container col-md-5">
 
-<div class="login-dialog" id="login-dialog">
+{#
+ # Remove default contents
+ #}
+{% block html_head_loading_legacy %}
+{% endblock %}
+{% block html_head_loading_app %}
+{% endblock %}
+{% block layout_head_nav %}
+{% endblock %}
+{% block sidebar %}
+{% endblock %}
 
-  <div class="login-dialog-inner front">
-    {% if reason === 'registered'%}
 
-      <h2>登録完了</h2>
 
-      <p class="text-center">
-        <i class="fa fa-smile-o fa-3x"></i>
-      </p>
-      <hr>
-      <div class="alert alert-info text-center">
-        {{ reasonMessage }}
-      </div>
+{% block layout_main %}
+
+<div class="main container-fluid">
 
-    {% else %}
+  <div class="row">
 
-      <h2>ログインエラー</h2>
+    <div class="login-header col-sm-offset-4 col-sm-4">
+      <div class="logo">{% include '../widget/logo.html' %}</div>
+      <h1>GROWI</h1>
 
-      <p class="text-center">
-        <i class="fa fa-meh-o fa-3x"></i>
-      </p>
-      <hr>
-      {% if reasonMessage != '' %}
-      <div class="alert alert-danger text-center">
-        {{ reasonMessage }}
+      <div class="m-b-15 login-form-errors text-center">
+        {% if reason === 'registered'%}
+        <div class="alert alert-success">
+          <h2>登録完了</h2>
+        </div>
+        {% else %}
+        <div class="alert alert-warning">
+            <h2>ログインエラー</h2>
+        </div>
+        {% endif %}
       </div>
-      {% endif %}
 
-    {% endif %}
+      <p>{{ reasonMessage }}</p>
+    </div>
 
-  </div>
 
-</div>
+  </div>{# /.row #}
 
-</div>
+</div>{# /.main #}
 
 {% endblock %}
-

+ 12 - 10
lib/views/me/api_token.html

@@ -1,12 +1,12 @@
-{% extends '../layout/2column.html' %}
+{% extends '../layout-growi/base/layout.html' %}
 
 
 {% block html_title %}{{ t('API Settings') }} · {{ path }}{% endblock %}
 
-{% block content_head %}
+{% block content_header %}
 <div class="header-wrap">
   <header id="page-header">
-  <h1 class="title" id="">{{ t('User Settings') }}</h1>
+  <h1 class="title" id="">{{ t('API Settings') }}</h1>
   </header>
 </div>
 {% endblock %}
@@ -15,10 +15,12 @@
 <div class="content-main">
 
   <ul class="nav nav-tabs">
-    <li><a href="/me"><i class="fa fa-gears"></i> {{ t('User Information') }}</a></li>
-    <li><a href="/me/external-accounts"><i class="fa fa-user-plus"></i> {{ t('External Accounts') }}</a></li>
-    <li><a href="/me/password"><i class="fa fa-key"></i> {{ t('Password Settings') }}</a></li>
-    <li class="active"><a href="/me/apiToken"><i class="fa fa-rocket"></i> {{ t('API Settings') }}</a></li>
+    <li><a href="/me"><i class="icon-user"></i> {{ t('User Information') }}</a></li>
+    {% if isEnabledPassport() %}
+    <li><a href="/me/external-accounts"><i class="icon-share-alt"></i> {{ t('External Accounts') }}</a></li>
+    {% endif %}
+    <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">
@@ -40,7 +42,7 @@
   </div>
   {% endif %}
 
-  <div id="form-box">
+  <div class="form-box m-t-20">
 
     <form action="/me/apiToken" method="post" class="form-horizontal" role="form">
     <fieldset>
@@ -59,7 +61,7 @@
       </div>
 
       <div class="form-group">
-        <div class="col-xs-offset-3 col-xs-10">
+        <div class="col-xs-offset-3 col-xs-9">
 
           <p class="alert alert-warning">
             {{ t('page_me_apitoken.notice.update_token1') }}<br>
@@ -82,5 +84,5 @@
 {% block content_footer %}
 {% endblock %}
 
-{% block footer %}
+{% block layout_footer %}
 {% endblock %}

+ 21 - 21
lib/views/me/external-accounts.html

@@ -1,11 +1,11 @@
-{% extends '../layout/2column.html' %}
+{% extends '../layout-growi/base/layout.html' %}
 
 {% block html_title %}{{ t('Password Settings') }} · {{ path }}{% endblock %}
 
-{% block content_head %}
+{% block content_header %}
 <div class="header-wrap">
   <header id="page-header">
-    <h1 class="title" id="">{{ t('User Settings') }}</h1>
+    <h1 class="title" id="">{{ t('Password Settings') }}</h1>
   </header>
 </div>
 {% endblock %}
@@ -14,10 +14,10 @@
 <div class="content-main">
 
   <ul class="nav nav-tabs">
-    <li><a href="/me"><i class="fa fa-gears"></i> {{ t('User Information') }}</a></li>
-    <li class="active"><a href="/me/external-accounts"><i class="fa fa-user-plus"></i> {{ t('External Accounts') }}</a></li>
-    <li><a href="/me/password"><i class="fa fa-key"></i> {{ t('Password Settings') }}</a></li>
-    <li><a href="/me/apiToken"><i class="fa fa-rocket"></i> {{ t('API Settings') }}</a></li>
+    <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">
@@ -58,9 +58,9 @@
 
 
 
-  <legend style="line-height: 1.7em;">
+  <legend class="m-t-20" style="line-height: 1.7em;">
     <button class="btn btn-default btn-sm pull-right" data-target="#create-external-account" data-toggle="modal">
-      <i class="fa fa-plus-circle" aria-hidden="true"></i>
+      <i class="icon-plus" aria-hidden="true"></i>
       Add
     </button>
     {{ t('External Accounts') }}
@@ -68,7 +68,7 @@
 
   <div class="row">
     <div class="col-md-12">
-      <table class="table table-hover table-striped table-bordered table-user-list">
+      <table class="table table-bordered table-user-list">
         <thead>
           <tr>
             <th width="120px">Authentication Provider</th>
@@ -111,37 +111,37 @@
     <div class="modal-dialog">
       <div class="modal-content">
 
-        <div class="modal-header">
+        <div class="modal-header bg-info">
           <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
-          <h4 class="modal-title">{{ t('Create External Account') }}</h4>
+          <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>
+              <a href="#passport-ldap" data-toggle="tab" role="tab"><i class="icon-organization"></i> LDAP</a>
             </li>
             <li>
-              <a href="#passport-google-oauth" data-toggle="tab" role="tab"><i class="fa fa-google"></i> Google OAuth</a>
+              <a href="#passport-google-oauth" data-toggle="tab" role="tab"><i class="icon-social-google"></i> Google OAuth</a>
             </li>
             <li>
-              <a href="#passport-facebook" data-toggle="tab" role="tab"><i class="fa fa-facebook"></i> Facebook</a>
+              <a href="#passport-facebook" data-toggle="tab" role="tab"><i class="icon-social-facebook"></i> Facebook</a>
             </li>
             <li>
-              <a href="#passport-twitter" data-toggle="tab" role="tab"><i class="fa fa-twitter"></i> Twitter</a>
+              <a href="#passport-twitter" data-toggle="tab" role="tab"><i class="icon-social-twitter"></i> Twitter</a>
             </li>
             <li>
-              <a href="#passport-github" data-toggle="tab" role="tab"><i class="fa fa-github"></i> Github</a>
+              <a href="#passport-github" data-toggle="tab" role="tab"><i class="icon-social-github"></i> Github</a>
             </li>
           </ul>
 
-          <div class="tab-content passport-settings">
+          <div class="tab-content passport-settings m-t-15">
             <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-primary pull-right" onclick="associateLdap()">
+                  <button type="button" class="btn btn-info pull-right" onclick="associateLdap()">
                     <i class="fa fa-plus-circle" aria-hidden="true"></i>
                     {{ t('Add') }}
                   </button>
@@ -192,7 +192,7 @@
 
         <div class="modal-header">
           <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
-          <h4 class="modal-title">{{ t('Diassociate External Account') }}</h4>
+          <div class="modal-title">{{ t('Diassociate External Account') }}</div>
         </div>
 
         <div class="modal-body">
@@ -247,5 +247,5 @@
 {% block content_footer %}
 {% endblock %}
 
-{% block footer %}
+{% block layout_footer %}
 {% endblock %}

+ 63 - 75
lib/views/me/index.html

@@ -1,8 +1,8 @@
-{% extends '../layout/2column.html' %}
+{% extends '../layout-growi/base/layout.html' %}
 
 {% block html_title %}{{ t('User Settings') }} · {{ path }}{% endblock %}
 
-{% block content_head %}
+{% block content_header %}
 <div class="header-wrap">
   <header id="page-header">
     <h1 class="title" id="">{{ t('User Settings') }}</h1>
@@ -14,10 +14,12 @@
 <div class="content-main">
 
   <ul class="nav nav-tabs">
-    <li class="active"><a href="/me"><i class="fa fa-gears"></i> {{ t('User Information') }}</a></li>
-    <li><a href="/me/external-accounts"><i class="fa fa-user-plus"></i> {{ t('External Accounts') }}</a></li>
-    <li><a href="/me/password"><i class="fa fa-key"></i> {{ t('Password Settings') }}</a></li>
-    <li><a href="/me/apiToken"><i class="fa fa-rocket"></i> {{ t('API Settings') }}</a></li>
+    <li class="active"><a href="/me"><i class="icon-user"></i> {{ t('User Information') }}</a></li>
+    {% if isEnabledPassport() %}
+    <li><a href="/me/external-accounts"><i class="icon-share-alt"></i> {{ t('External Accounts') }}</a></li>
+    {% endif %}
+    <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">
@@ -47,7 +49,7 @@
   {% endif %}
 
 
-  <div class="form-box">
+  <div class="form-box m-t-20">
     <form action="/me" method="post" class="form-horizontal" role="form">
       <fieldset>
         <legend>{{ t('Basic Info') }}</legend>
@@ -91,7 +93,7 @@
     </form>
   </div>
 
-  <div class="form-box">
+  <div class="form-box m-t-20">
 
     <!-- separeted form tag -->
     <form action="/me/imagetype" id="formImageType" method="post" class="form" role="form"></form>
@@ -100,13 +102,13 @@
 
       <legend>{{ t('Set Profile Image') }}</legend>
 
-      <div class="form-group col-sm-offset-1 col-sm-3">
+      <div class="form-group col-md-2 col-sm-offset-1 col-sm-4">
         <div class="radio">
           <h4>
             <input type="radio" form="formImageType" name="imagetypeForm[isGravatarEnabled]" value="true" {% if user.isGravatarEnabled %}checked="checked"{% endif %}>
             <img src="https://gravatar.com/avatar/00000000000000000000000000000000?s=24" /> Gravatar
             <a href="https://gravatar.com/">
-              <small><i class="fa fa-external-link" aria-hidden="true"></i></small>
+              <small><i class="icon-arrow-right-circle" aria-hidden="true"></i></small>
             </a>
           </h4>
         </div>
@@ -114,7 +116,7 @@
         <img src="{{ user|gravatar }}" width="64">
       </div><!-- /.col-sm* -->
 
-      <div class="form-group col-sm-8">
+      <div class="form-group col-md-4 col-sm-7">
         <div class="radio">
           <h4>
             <input type="radio" form="formImageType" name="imagetypeForm[isGravatarEnabled]" value="false" {% if !user.isGravatarEnabled  %}checked="checked"{% endif %}>
@@ -128,7 +130,7 @@
           </label>
           <div class="col-sm-8">
             <p>
-            <img src="{{ user|uploadedpicture }}" width="64" id="settingUserPicture"><br>
+            <img src="{{ user|uploadedpicture }}" class="picture picture-lg img-circle" id="settingUserPicture"><br>
             </p>
             <p>
             {% if user.image %}
@@ -148,7 +150,7 @@
             {% if isUploadable() %}
             <form action="/_api/me/picture/upload" id="pictureUploadForm" method="post" class="form-horizontal" role="form" enctype="multipart/form-data">
               <input name="userPicture" type="file" accept="image/*">
-              <div id="pictureUploadFormProgress">
+              <div id="pictureUploadFormProgress" class="d-flex align-items-center">
               </div>
             </form>
             {% else %}
@@ -179,7 +181,7 @@
         return false;
       }
 
-      $('#pictureUploadFormProgress').html('<img src="/images/loading_s.gif"> アップロード中...');
+      $('#pictureUploadFormProgress').html('<div class="speeding-wheel-sm m-r-5"></div> アップロード中...');
       $.ajax($form.attr("action"), {
         type: 'post',
         processData: false,
@@ -205,68 +207,54 @@
   });
   </script>
 
-  <div class="row">
-    {% if googleLoginEnabled() %}
-
-    <div class="col-sm-6"> {# Google Connect #}
-
-      <div class="form-box">
-        <form action="/me/auth/google" method="post" class="form-horizontal" role="form">
-          <fieldset>
-            <legend><i class="fa fa-google-plus-square"></i> {{ t('Google Setting') }}</legend>
-
-            {% set wmessage = req.flash('warningMessage.auth.google') %}
-            {% if wmessage.length %}
-            <div class="alert alert-danger">
-              {{ wmessage }}
-            </div>
-            {% endif %}
-
-            <div class="form-group">
-            {% if user.googleId %}
-
-            <div class="col-sm-12">
-              <p>
-                {{ t('Connected') }}
-
-                <input type="submit" name="disconnectGoogle" class="btn btn-default" value="{{ t('Disconnect') }}">
-              </p>
-              <p class="help-block">
-                {{ t('page_me.form_help.google_disconnect1') }}<br>
-                {{ t('page_me.form_help.google_disconnect2') }}
-              </p>
-            </div>
-
-            {% else %}
-
-            <div class="col-sm-12">
-              <div class="text-center">
-                <input type="submit" name="connectGoogle" class="btn btn-google" value="Googleコネクト">
-              </div>
-              <p class="help-block">
-                {{ t('page_me.form_help.google_connect1') }}<br>
-              </p>
-              {% if config.crowi['security:registrationWhiteList'] && config.crowi['security:registrationWhiteList'].length %}
-              <p class="help-block">
-                {{ t('page_register.form_help.email') }}<br>
-                {{ t('page_me.form_help.google_connect2') }}
-              </p>
-              <ul>
-                {% for em in config.crowi['security:registrationWhiteList'] %}
-                <li><code>{{ em }}</code></li>
-                {% endfor %}
-              </ul>
-              {% endif %}
-            </div>
+  {% if googleLoginEnabled() %}
+  <div class="form-box">
+    <legend>{{ t('Google Setting') }}</legend>
+    <form action="/me/auth/google" method="post" class="form-horizontal col-sm-12" role="form">
+      <fieldset>
+        {% set wmessage = req.flash('warningMessage.auth.google') %}
+        {% if wmessage.length %}
+        <div class="alert alert-danger">
+          {{ wmessage }}
+        </div>
+        {% endif %}
 
-            {% endif %}
+        <div class="form-group">
+        {% if user.googleId %}
+        <div>
+          <p>
+            <input type="submit" name="disconnectGoogle" class="btn btn-default" value="{{ t('Disconnect') }}">
+          </p>
+          <p class="help-block">
+            {{ t('page_me.form_help.google_disconnect1') }}<br>
+            {{ t('page_me.form_help.google_disconnect2') }}
+          </p>
+        </div>
+        {% else %}
+        <div>
+          <div class="text-center">
+            <input type="submit" name="connectGoogle" class="btn btn-google" value="Googleコネクト">
           </div>
-        </fieldset>
-      </form>
-    </div> {# /Google Connect #}
-    {% endif %}
-
+          <p class="help-block">
+            {{ t('page_me.form_help.google_connect1') }}<br>
+          </p>
+          {% if config.crowi['security:registrationWhiteList'] && config.crowi['security:registrationWhiteList'].length %}
+          <p class="help-block">
+            {{ t('page_register.form_help.email') }}<br>
+            {{ t('page_me.form_help.google_connect2') }}
+          </p>
+          <ul>
+            {% for em in config.crowi['security:registrationWhiteList'] %}
+            <li><code>{{ em }}</code></li>
+            {% endfor %}
+          </ul>
+          {% endif %}
+        </div>
+        {% endif %}
+      </fieldset>
+    </form>
   </div>
+  {% endif %}
 
   </div> {# end of .tab-contents #}
 
@@ -305,5 +293,5 @@
 {% block content_footer %}
 {% endblock content_footer %}
 
-{% block footer %}
-{% endblock footer %}
+{% block layout_footer %}
+{% endblock layout_footer %}

+ 12 - 16
lib/views/me/password.html

@@ -1,11 +1,11 @@
-{% extends '../layout/2column.html' %}
+{% extends '../layout-growi/base/layout.html' %}
 
 {% block html_title %}{{ t('Password Settings') }} · {{ path }}{% endblock %}
 
-{% block content_head %}
+{% block content_header %}
 <div class="header-wrap">
   <header id="page-header">
-    <h1 class="title" id="">{{ t('User Settings') }}</h1>
+    <h1 class="title" id="">{{ t('Password Settings') }}</h1>
   </header>
 </div>
 {% endblock %}
@@ -14,10 +14,12 @@
 <div class="content-main">
 
   <ul class="nav nav-tabs">
-    <li><a href="/me"><i class="fa fa-gears"></i> {{ t('User Information') }}</a></li>
-    <li><a href="/me/external-accounts"><i class="fa fa-user-plus"></i> {{ t('External Accounts') }}</a></li>
-    <li class="active"><a href="/me/password"><i class="fa fa-key"></i> {{ t('Password Settings') }}</a></li>
-    <li><a href="/me/apiToken"><i class="fa fa-rocket"></i> {{ t('API Settings') }}</a></li>
+    <li><a href="/me"><i class="icon-user"></i> {{ t('User Information') }}</a></li>
+    {% if isEnabledPassport() %}
+    <li><a href="/me/external-accounts"><i class="icon-share-alt"></i> {{ t('External Accounts') }}</a></li>
+    {% endif %}
+    <li class="active"><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">
@@ -45,13 +47,7 @@
   </div>
   {% endif %}
 
-  {% if user.email %}
-  <p>
-    {{ t('You can sign in with email and password', user.email) }}
-  </p>
-  {% endif %}
-
-  <div id="form-box">
+  <div id="form-box" class="m-t-20">
 
     <form action="/me/password" method="post" class="form-horizontal" role="form">
     <fieldset>
@@ -85,7 +81,7 @@
 
 
       <div class="form-group">
-        <div class="col-xs-offset-2 col-xs-10">
+        <div class="text-center">
           <button type="submit" class="btn btn-primary">{{ t('Update') }}</button>
         </div>
       </div>
@@ -102,5 +98,5 @@
 {% block content_footer %}
 {% endblock %}
 
-{% block footer %}
+{% block layout_footer %}
 {% endblock %}

+ 29 - 31
lib/views/modal/create_page.html

@@ -2,49 +2,47 @@
   <div class="modal-dialog">
     <div class="modal-content">
 
-      <div class="modal-header">
+      <div class="modal-header bg-primary">
         <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
-        <h4 class="modal-title">{{ t('New Page') }}</h4>
+        <div class="modal-title">{{ t('New Page') }}</div>
       </div>
 
       <div class="modal-body">
 
-        <form class="form-horizontal" id="create-page-today" role="form">
-          <fieldset>
-            <div class="col-xs-12">
-              <h4>{{ t("Create today's") }}</h4>
-            </div>
-            <div class="col-xs-10">
-              <span class="page-today-prefix">{{ userPageRoot(user) }}/</span>
-              <input type="text" data-prefix="{{ userPageRoot(user) }}/" class="page-today-input1 form-control text-center" value="{{ t('Memo') }}" id="" name="">
-              <span class="page-today-suffix">/{{ now|datetz('Y/m/d') }}/</span>
-              <input type="text" data-prefix="/{{ now|datetz('Y/m/d') }}/" class="page-today-input2 form-control" id="page-today-input2" name="" placeholder="{{ t('Input page name (optional)') }}">
-            </div>
-            <div class="col-xs-2">
-              <button type="submit" class="btn btn-primary">{{ t('Create') }}</button>
+        <form class="row form-horizontal" id="create-page-today" role="form">
+          <fieldset class="col-xs-12">
+            <legend>{{ t("Create today's") }}</legend>
+            <div class="d-flex create-page-input-container">
+              <div class="create-page-input-row d-flex align-items-center">
+                <span class="page-today-prefix">{{ userPageRoot(user) }}/</span>
+                <input type="text" data-prefix="{{ userPageRoot(user) }}/" class="page-today-input1 form-control text-center" value="{{ t('Memo') }}" id="" name="">
+                <span class="page-today-suffix">/{{ now|datetz('Y/m/d') }}/</span>
+                <input type="text" data-prefix="/{{ now|datetz('Y/m/d') }}/" class="page-today-input2 form-control" id="page-today-input2" name="" placeholder="{{ t('Input page name (optional)') }}">
+              </div>
+              <div class="create-page-button-container">
+                <button type="submit" class="fcbtn btn btn-outline btn-rounded btn-primary btn-1b"><i class="icon-fw icon-doc"></i>{{ t('Create') }}</button>
+              </div>
             </div>
           </fieldset>
         </form>
-        <hr>
 
-        <form class="form-horizontal" id="create-page-under-tree" role="form">
-          <fieldset>
-            <div class="col-xs-12 create-page-under-tree-label">
-              <h4>{{ t('Create under', parentPath(path)) }}</h4>
-            </div>
-            <div class="col-xs-10">
-              {% if searchConfigured() %}
-              <div class="clearfix" id="page-name-inputter"></div>
-              {% else %}
-              <input type="text" value="{{ parentPath(path) }}" class="page-name-input form-control " placeholder="{{ t('Input page name') }}" required />
-              {% endif %}
-            </div>
-            <div class="col-xs-2">
-              <button type="submit" class="btn btn-primary">{{ t('Create') }}</button>
+        <form class="row form-horizontal m-t-15" id="create-page-under-tree" role="form">
+          <fieldset class="col-xs-12">
+            <legend>{{ t('Create under', parentPath(path)) }}</legend>
+            <div class="d-flex create-page-input-container">
+              <div class="create-page-input-row d-flex align-items-center">
+                {% if searchConfigured() %}
+                <div id="page-name-inputter"></div>
+                {% else %}
+                <input type="text" value="{{ parentPath(path) }}" class="page-name-input form-control " placeholder="{{ t('Input page name') }}" required />
+                {% endif %}
+              </div>
+              <div class="create-page-button-container">
+                <button type="submit" class="fcbtn btn btn-outline btn-rounded btn-primary btn-1b"><i class="icon-fw icon-doc"></i>{{ t('Create') }}</button>
+              </div>
             </div>
           </fieldset>
         </form>
-        <hr>
 
       </div><!-- /.modal-body -->
 

+ 36 - 20
lib/views/modal/delete.html

@@ -4,9 +4,15 @@
 
       <form role="form" id="delete-page-form" onsubmit="return false;">
 
-        <div class="modal-header">
+        <div class="modal-header {% if page.isDeleted() %}bg-danger{% endif %}">
           <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
-          <h4 class="modal-title"><i class="fa fa-trash-o"></i> {{ t('modal_delete.label.Delete Page') }}</h4>
+          <div class="modal-title">
+            {% if page.isDeleted() %}
+            <i class="icon-fw icon-fire"></i> {{ t('modal_delete.label.completely') }}
+            {% else %}
+            <i class="icon-fw icon-trash"></i> {{ t('modal_delete.label.Delete Page') }}
+            {% endif %}
+          </div>
         </div>
         <div class="modal-body">
           <div class="form-group">
@@ -15,24 +21,34 @@
           </div>
         </div>
         <div class="modal-footer">
-          <p><small class="pull-left" id="delete-errors"></small></p>
-          <input type="hidden" name="_csrf" value="{{ csrf() }}">
-          <input type="hidden" name="path" value="{{ page.path }}">
-          <input type="hidden" name="page_id" value="{{ page._id.toString() }}">
-          <input type="hidden" name="revision_id" value="{{ page.revision._id.toString() }}">
-          <label class="checkbox-inline text-danger">
-            <input type="checkbox" name="recursively">{{ t('modal_delete.label.recursively') }}
-          </label>
-          {% if page.isDeleted() %}
-            <input type="hidden" name="completely" value="true">
-            <button type="submit" class="btn btn-danger delete-button"><i class="fa fa-times-circle" aria-hidden="true"></i> {{ t('Delete Completely') }}</button>
-          {% else %}
-            <label class="checkbox-inline text-danger">
-              <input type="checkbox" name="completely">{{ t('modal_delete.label.completely') }}
-            </label>
-            <button type="submit" class="btn btn-danger delete-button">Delete</button>
-          {% endif %}
-        </div>
+          <div class="d-flex justify-content-between">
+            <p><small id="delete-errors"></small></p>
+            <div>
+              <input type="hidden" name="_csrf" value="{{ csrf() }}">
+              <input type="hidden" name="path" value="{{ page.path }}">
+              <input type="hidden" name="page_id" value="{{ page._id.toString() }}">
+              <input type="hidden" name="revision_id" value="{{ page.revision._id.toString() }}">
+              <label class="checkbox-inline">
+                <input type="checkbox" name="recursively">{{ t('modal_delete.label.recursively') }}
+              </label>
+              {% if page.isDeleted() %}
+                <input type="hidden" name="completely" value="true">
+                <button type="submit" class="m-l-10 btn btn-sm btn-danger delete-button">
+                  <i class="icon-fire" aria-hidden="true"></i>
+                  {{ t('Delete Completely') }}
+                </button>
+              {% else %}
+                <label class="checkbox-inline text-danger">
+                  <input type="checkbox" name="completely">{{ t('modal_delete.label.completely') }}
+                </label>
+                <button type="submit" class="m-l-10 btn btn-sm btn-default delete-button">
+                  <i class="icon-trash" aria-hidden="true"></i>
+                  {{ t('Delete') }}
+                </button>
+              {% endif %}
+            </div>
+          </div>
+        </div><!-- /.modal-footer -->
 
       </form>
       </div><!-- /.modal-content -->

+ 17 - 8
lib/views/modal/duplicate.html

@@ -4,9 +4,9 @@
 
       <form role="form" id="duplicatePageForm" onsubmit="return false;">
 
-        <div class="modal-header">
+        <div class="modal-header bg-primary">
           <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
-          <h4 class="modal-title">{{ t('modal_duplicate.label.Duplicate page') }}</h4>
+          <div class="modal-title">{{ t('modal_duplicate.label.Duplicate page') }}</div>
         </div>
         <div class="modal-body">
             <div class="form-group">
@@ -22,12 +22,21 @@
             </div>
         </div>
         <div class="modal-footer">
-          <p><small class="pull-left" id="duplicatePageNameCheck"></small></p>
-          <input type="hidden" name="_csrf" value="{{ csrf() }}">
-          <input type="hidden" name="path" value="{{ page.path }}">
-          <input type="hidden" name="page_id" value="{{ page._id.toString() }}">
-          <input type="hidden" name="revision_id" value="{{ page.revision._id.toString() }}">
-          <input type="submit" class="btn btn-primary" value="Duplicate page">
+          <div class="d-flex justify-content-between">
+            <p>
+              <span class="text-danger msg-already-exists">
+                <strong><i class="icon-fw icon-ban"></i>{{ t('Page is already exists.') }}</strong>
+              </span>
+              <small id="linkToNewPage" class="msg-already-exists"></small>
+            </p>
+            <div>
+              <input type="hidden" name="_csrf" value="{{ csrf() }}">
+              <input type="hidden" name="path" value="{{ page.path }}">
+              <input type="hidden" name="page_id" value="{{ page._id.toString() }}">
+              <input type="hidden" name="revision_id" value="{{ page.revision._id.toString() }}">
+              <button type="submit" class="btn btn-primary">Duplicate page</button>
+            </div>
+          </div>
         </div>
 
       </form>

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů