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

Merge pull request #326 from weseek/master

release v3.0.0
Yuki Takei 8 лет назад
Родитель
Сommit
d9f09aedf5
100 измененных файлов с 5046 добавлено и 1811 удалено
  1. 2 1
      .eslintrc.js
  2. 4 4
      .github/ISSUE_TEMPLATE.md
  3. 3 2
      .gitignore
  4. 17 1
      CHANGES.md
  5. 49 66
      README.md
  6. 14 4
      THIRD-PARTY-NOTICES.md
  7. 5 5
      app.js
  8. 5 5
      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. 2 1
      lib/form/me/user.js
  21. 655 0
      lib/locales/en-US/sandbox.md
  22. 13 6
      lib/locales/en-US/translation.json
  23. 29 0
      lib/locales/en-US/welcome.md
  24. 655 0
      lib/locales/ja/sandbox.md
  25. 16 7
      lib/locales/ja/translation.json
  26. 29 0
      lib/locales/ja/welcome.md
  27. 34 8
      lib/models/config.js
  28. 3 0
      lib/models/index.js
  29. 280 0
      lib/models/page-group-relation.js
  30. 75 12
      lib/models/page.js
  31. 1 1
      lib/models/revision.js
  32. 279 0
      lib/models/user-group-relation.js
  33. 151 0
      lib/models/user-group.js
  34. 12 2
      lib/models/user.js
  35. 8 7
      lib/plugins/plugin-utils.js
  36. 315 7
      lib/routes/admin.js
  37. 15 3
      lib/routes/index.js
  38. 25 7
      lib/routes/installer.js
  39. 3 2
      lib/routes/login.js
  40. 3 2
      lib/routes/me.js
  41. 44 98
      lib/routes/page.js
  42. 2 1
      lib/util/mailer.js
  43. 2 2
      lib/util/middlewares.js
  44. 1 1
      lib/util/search.js
  45. 1 1
      lib/util/slack.js
  46. 39 3
      lib/util/swigFunctions.js
  47. 20 18
      lib/views/_form.html
  48. 25 21
      lib/views/admin/app.html
  49. 163 51
      lib/views/admin/customize.html
  50. 18 16
      lib/views/admin/external-accounts.html
  51. 10 6
      lib/views/admin/index.html
  52. 10 10
      lib/views/admin/markdown.html
  53. 85 79
      lib/views/admin/notification.html
  54. 45 40
      lib/views/admin/search.html
  55. 41 33
      lib/views/admin/security.html
  56. 248 0
      lib/views/admin/user-group-detail.html
  57. 174 0
      lib/views/admin/user-groups.html
  58. 86 64
      lib/views/admin/users.html
  59. 9 10
      lib/views/admin/widget/menu.html
  60. 5 5
      lib/views/admin/widget/passport/google-oauth.html
  61. 7 7
      lib/views/admin/widget/passport/ldap.html
  62. 7 0
      lib/views/admin/widget/theme-colorbox.html
  63. 0 48
      lib/views/crowi-plus/base/not_found_nosidebar.html
  64. 0 48
      lib/views/crowi-plus/base/page_list_nosidebar.html
  65. 0 48
      lib/views/crowi-plus/base/page_nosidebar.html
  66. 0 72
      lib/views/crowi-plus/base/user_page_nosidebar.html
  67. 0 34
      lib/views/crowi-plus/not_found.html
  68. 0 57
      lib/views/crowi-plus/page.html
  69. 0 48
      lib/views/crowi-plus/page_list.html
  70. 0 24
      lib/views/crowi-plus/user_page.html
  71. 0 69
      lib/views/crowi-plus/widget/header.html
  72. 3 3
      lib/views/customlayout-selector/not_found.html
  73. 3 3
      lib/views/customlayout-selector/page.html
  74. 3 3
      lib/views/customlayout-selector/page_list.html
  75. 3 3
      lib/views/customlayout-selector/user_page.html
  76. 0 15
      lib/views/index.html
  77. 82 58
      lib/views/installer.html
  78. 91 75
      lib/views/invited.html
  79. 52 0
      lib/views/layout-crowi/base/layout.html
  80. 41 0
      lib/views/layout-crowi/not_found.html
  81. 68 0
      lib/views/layout-crowi/page.html
  82. 98 0
      lib/views/layout-crowi/page_list.html
  83. 19 0
      lib/views/layout-crowi/user_page.html
  84. 5 5
      lib/views/layout-crowi/widget/page_side_content.html
  85. 6 6
      lib/views/layout-crowi/widget/page_side_header.html
  86. 32 0
      lib/views/layout-growi/base/layout.html
  87. 20 0
      lib/views/layout-growi/not_found.html
  88. 63 0
      lib/views/layout-growi/page.html
  89. 60 0
      lib/views/layout-growi/page_list.html
  90. 67 0
      lib/views/layout-growi/user_page.html
  91. 5 5
      lib/views/layout-growi/widget/comments.html
  92. 51 0
      lib/views/layout-growi/widget/header.html
  93. 0 57
      lib/views/layout/2column.html
  94. 5 1
      lib/views/layout/admin.html
  95. 128 112
      lib/views/layout/layout.html
  96. 0 22
      lib/views/layout/single-nologin.html
  97. 0 45
      lib/views/layout/single.html
  98. 251 206
      lib/views/login.html
  99. 35 38
      lib/views/login/error.html
  100. 16 13
      lib/views/me/api_token.html

+ 2 - 1
.eslintrc.js

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

+ 4 - 4
.github/ISSUE_TEMPLATE.md

@@ -6,15 +6,15 @@ Environment
 | item     | version |
 | item     | version |
 | ---      | --- |
 | ---      | --- |
 |OS        ||
 |OS        ||
-|crowi-plus|x.y.z|
+|GROWI     |x.y.z|
 |node.js   |x.y.z|
 |node.js   |x.y.z|
 |npm       |x.y.z|
 |npm       |x.y.z|
 |Using Docker|yes/no|
 |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
 ### Client

+ 3 - 2
.gitignore

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

+ 17 - 1
CHANGES.md

@@ -1,7 +1,23 @@
 CHANGES
 CHANGES
 ========
 ========
 
 
-## 2.4.4-RC
+## 3.0.0-RC
+
+* Feature: Group Access Control List
+* Feature: Add theme selector
+* Feature: Custom title tag content
+* Feature: Add a control to switch whether email shown or hidden by user
+* Fix: bosai version
+* Support: Rename to GROWI
+* 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
 
 
 * Feature: Autoformat Markdown Table
 * Feature: Autoformat Markdown Table
 * Feature: highlight.js Theme Selector
 * Feature: highlight.js Theme Selector

+ 49 - 66
README.md

@@ -1,55 +1,40 @@
-![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://demo.growi.org">Demo Site</a>
+</p>
 <p align="center">
 <p align="center">
   <a href="https://heroku.com/deploy"><img src="https://www.herokucdn.com/deploy/button.png"></a>
   <a href="https://heroku.com/deploy"><img src="https://www.herokucdn.com/deploy/button.png"></a>
 </p>
 </p>
 <p align="center">
 <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://growi-slackin.weseek.co.jp/"><img src="https://crowi-plus-slackin.weseek.co.jp/badge.svg"></a>
 </p>
 </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)
-
+[![wercker status](https://app.wercker.com/status/595b761d0e26796ddb304679f7bf27de/s/master "wercker status")](https://app.wercker.com/project/byKey/595b761d0e26796ddb304679f7bf27de)
+[![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**
 * **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 
   * Support Authentication with LDAP / Active Directory 
   * Slack Incoming Webhooks Integration
   * 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 Ready][dockerhub]**
 * **[Docker Compose Ready][docker-compose]**
 * **[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)
 * 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
 Quick Start for Production
 ===========================
 ===========================
@@ -64,26 +49,26 @@ Using docker-compose
 ---------------------
 ---------------------
 
 
 ```bash
 ```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
 docker-compose up
 ```
 ```
 
 
-See also [weseek/crowi-plus-docker-compose][docker-compose]
+See also [weseek/growi-docker-compose][docker-compose]
 
 
 On-premise
 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
 ### 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
 - yarn
 - MongoDB 3.x
 - 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
 #### Optional Dependencies
 
 
@@ -98,24 +83,24 @@ See [confirmed versions](https://github.com/weseek/crowi-plus/wiki/Developers-Gu
 #### Build and run the app
 #### Build and run the app
 
 
 ```bash
 ```bash
-git clone https://github.com/weseek/crowi-plus.git
-cd crowi-plus
+git clone https://github.com/weseek/growi.git
+cd growi
 yarn
 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.
 **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
 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
 #### Command details
 
 
@@ -137,19 +122,19 @@ npm start
 
 
 * Stop server if server is running
 * Stop server if server is running
 * `yarn add` to install plugin or `npm install --save`
 * `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
 * `npm start` to build client app and start server
 
 
 #### Examples
 #### Examples
 
 
 ```bash
 ```bash
-yarn add crowi-plugin-lsx
+yarn add growi-plugin-lsx
 npm start
 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
 Environment Variables
@@ -171,10 +156,10 @@ Environment Variables
 Documentation
 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
 Contribution
 ============
 ============
@@ -185,7 +170,6 @@ For development
 ### Build and Run the app
 ### Build and Run the app
 
 
 1. `clone` this repository
 1. `clone` this repository
-1. `yarn global add npm@4` to install required global dependencies
 1. `yarn` to install all dependencies
 1. `yarn` to install all dependencies
     * DO NOT USE `npm install`
     * DO NOT USE `npm install`
 1. `npm run build` to build client app
 1. `npm run build` to build client app
@@ -196,7 +180,7 @@ Found a Bug?
 -------------
 -------------
 
 
 If you found a bug in the source code, you can help us by
 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.
 [submit a Pull Request][pulls] with a fix.
 
 
 Missing a Feature?
 Missing a Feature?
@@ -217,7 +201,7 @@ You can write issues and PRs in English or Japanese.
 Discussion
 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
 License
@@ -228,9 +212,8 @@ License
 
 
 
 
 [crowi]: https://github.com/crowi/crowi
 [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,  
 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 :
 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.
 The attached notices are provided for information only.
 
 
 For any licenses that require disclosure of source, sources are available at  
 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)
 1. crowi/crowi (https://github.com/crowi/crowi)
 2. Microsoft/vscode (https://github.com/Microsoft/vscode)
 2. Microsoft/vscode (https://github.com/Microsoft/vscode)
-
+3. stephenhutchings/typicons.font (https://github.com/stephenhutchings/typicons.font)
 
 
 
 
 License Notice for Crowi
 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.
 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>
  * @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);

+ 5 - 5
app.json

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

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

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

+ 5 - 5
config/env.dev.js

@@ -2,18 +2,18 @@ module.exports = {
   NODE_ENV: 'development',
   NODE_ENV: 'development',
   FILE_UPLOAD: 'local',
   FILE_UPLOAD: 'local',
   // MATHJAX: 1,
   // 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: [
   PLUGIN_NAMES_TOBE_LOADED: [
-    // 'crowi-plugin-lsx',
-    // 'crowi-plugin-pukiwiki-like-linker',
+    // 'growi-plugin-lsx',
+    // 'growi-plugin-pukiwiki-like-linker',
   ],
   ],
   // filters for debug
   // filters for debug
   DEBUG: [
   DEBUG: [
     // 'express:*',
     // 'express:*',
     // 'crowi:*',
     // 'crowi:*',
     'crowi:crowi',
     'crowi:crowi',
-    'crowi:crowi:dev',
+    // 'crowi:crowi:dev',
     'crowi:crowi:express-init',
     'crowi:crowi:express-init',
     'crowi:models:external-account',
     'crowi:models:external-account',
     // 'crowi:routes:login',
     // 'crowi:routes:login',

+ 15 - 9
config/webpack.common.js

@@ -10,7 +10,7 @@ const helpers = require('./helpers');
  */
  */
 const AssetsPlugin = require('assets-webpack-plugin');
 const AssetsPlugin = require('assets-webpack-plugin');
 const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');
 const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');
-const LodashModuleReplacementPlugin = require('lodash-webpack-plugin');;
+const ExtractTextPlugin = require('extract-text-webpack-plugin');
 
 
 /*
 /*
  * Webpack configuration
  * Webpack configuration
@@ -27,8 +27,10 @@ module.exports = function (options) {
       'legacy-admin':         './resource/js/legacy/crowi-admin',
       'legacy-admin':         './resource/js/legacy/crowi-admin',
       'legacy-presentation':  './resource/js/legacy/crowi-presentation',
       'legacy-presentation':  './resource/js/legacy/crowi-presentation',
       'plugin':               './resource/js/plugin',
       '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: {
     externals: {
       // require("jquery") is external and available
       // 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$/,
           test: /\.css$/,
           use: ['style-loader', 'css-loader'],
           use: ['style-loader', 'css-loader'],
-          // comment out 'include' spec for crowi-plugins
-          // include: [helpers.root('resource')]
+          exclude: [helpers.root('resource/styles/scss')]
         },
         },
         {
         {
           test: /\.scss$/,
           test: /\.scss$/,
           use: ['style-loader', 'css-loader', 'sass-loader'],
           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.
          * File loader for supporting images, for example, in CSS files.
@@ -102,8 +110,6 @@ module.exports = function (options) {
         chunks: ['commons', 'plugin'],
         chunks: ['commons', 'plugin'],
       }),
       }),
 
 
-      new LodashModuleReplacementPlugin,
-
       // ignore
       // ignore
       new webpack.IgnorePlugin(/^\.\/lib\/deflate\.js/, /markdown-it-plantuml/),
       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
  * Webpack Plugins
  */
  */
 const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');
 const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');
+const ExtractTextPlugin = require('extract-text-webpack-plugin');
 const DllBundlesPlugin = require('webpack-dll-bundles-plugin').DllBundlesPlugin;
 const DllBundlesPlugin = require('webpack-dll-bundles-plugin').DllBundlesPlugin;
 const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
 const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
 
 
@@ -51,6 +52,8 @@ module.exports = function (options) {
     },
     },
     plugins: [
     plugins: [
 
 
+      new ExtractTextPlugin('[name].bundle.css'),
+
       new DllBundlesPlugin({
       new DllBundlesPlugin({
         bundles: {
         bundles: {
           vendor: [
           vendor: [

+ 3 - 0
config/webpack.prod.js

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

+ 19 - 52
lib/crowi/dev.js

@@ -6,7 +6,7 @@ const helpers = require('./helpers');
 
 
 const swig = require('swig-templates');
 const swig = require('swig-templates');
 const onHeaders = require('on-headers')
 const onHeaders = require('on-headers')
-const LRWebSocketServer = require('livereload-server/lib/server');
+
 
 
 class CrowiDev {
 class CrowiDev {
 
 
@@ -21,11 +21,10 @@ class CrowiDev {
   }
   }
 
 
   init() {
   init() {
-    this.requireForLiveReload();
+    this.requireForAutoReloadServer();
 
 
     this.initPromiseRejectionWarningHandler();
     this.initPromiseRejectionWarningHandler();
     this.initSwig();
     this.initSwig();
-    this.hackLRWebSocketServer();
   }
   }
 
 
   initPromiseRejectionWarningHandler() {
   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
     // load all json files for live reloading
     fs.readdirSync(this.crowi.localeDir).map((dirname) => {
     fs.readdirSync(this.crowi.localeDir).map((dirname) => {
       require(path.join(this.crowi.localeDir, dirname, 'translation.json'));
       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) {
   setup(server, app) {
     this.setupHeaderDebugger(app);
     this.setupHeaderDebugger(app);
-    this.setupEasyLiveReload(app);
+    this.setupBrowserSync(app);
   }
   }
 
 
   setupHeaderDebugger(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) {
   loadPlugins(app) {

+ 19 - 6
lib/crowi/index.js

@@ -9,6 +9,7 @@ var debug = require('debug')('crowi:crowi')
 
 
   , mongoose    = require('mongoose')
   , mongoose    = require('mongoose')
 
 
+  , eazyLogger = require('eazy-logger')
   , helpers = require('./helpers')
   , helpers = require('./helpers')
   , models = require('../models')
   , models = require('../models')
   ;
   ;
@@ -16,6 +17,13 @@ var debug = require('debug')('crowi:crowi')
 function Crowi (rootdir, env)
 function Crowi (rootdir, env)
 {
 {
   var self = this;
   var self = this;
+  // this.logger = easyLogger.Logger({
+  //   prefix: '[{green:GROWI}]'
+  // });
+  this.logger = eazyLogger.Logger({
+    prefix: "[{green:GROWI}] ",
+    useLevelPrefixes: false,
+  });
 
 
   this.version = pkg.version;
   this.version = pkg.version;
   this.runtimeVersions = undefined;   // initialized by scanRuntimeVersions()
   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.MONGODB_URI || // MONGOLAB changes their env name
     this.env.MONGOHQ_URL ||
     this.env.MONGOHQ_URL ||
     this.env.MONGO_URI ||
     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(
   return mongoose.connect(mongoUri).then(
@@ -368,12 +376,17 @@ Crowi.prototype.start = function() {
     .then(function(app) {
     .then(function(app) {
       server = http.createServer(app).listen(self.port, function() {
       server = http.createServer(app).listen(self.port, function() {
         debug(`[${self.node_env}] Express server listening on port ${self.port}`);
         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 = require('socket.io')(server);
       io.sockets.on('connection', function (socket) {
       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'),
     customcss: require('./admin/customcss'),
     customscript: require('./admin/customscript'),
     customscript: require('./admin/customscript'),
     customheader: require('./admin/customheader'),
     customheader: require('./admin/customheader'),
+    customtheme: require('./admin/customtheme'),
+    customtitle: require('./admin/customtitle'),
     custombehavior: require('./admin/custombehavior'),
     custombehavior: require('./admin/custombehavior'),
     customlayout: require('./admin/customlayout'),
     customlayout: require('./admin/customlayout'),
     customfeatures: require('./admin/customfeatures'),
     customfeatures: require('./admin/customfeatures'),
@@ -30,5 +32,6 @@ module.exports = {
     userInvite: require('./admin/userInvite'),
     userInvite: require('./admin/userInvite'),
     slackIwhSetting: require('./admin/slackIwhSetting'),
     slackIwhSetting: require('./admin/slackIwhSetting'),
     slackSetting: require('./admin/slackSetting'),
     slackSetting: require('./admin/slackSetting'),
+    userGroupCreate: require('./admin/userGroupCreate'),
   },
   },
 };
 };

+ 2 - 1
lib/form/me/user.js

@@ -6,5 +6,6 @@ var form = require('express-form')
 module.exports = form(
 module.exports = form(
   field('userForm.name').trim().required(),
   field('userForm.name').trim().required(),
   field('userForm.email').trim().isEmail().required(),
   field('userForm.email').trim().isEmail().required(),
-  field('userForm.lang').required()
+  field('userForm.lang').required(),
+  field('userForm.isEmailPublished').trim().toBooleanStrict().required()
 );
 );

+ 655 - 0
lib/locales/en-US/sandbox.md

@@ -0,0 +1,655 @@
+<div class="panel panel-default">
+  <div class="panel-body">
+
+# 目次
+
+```
+@[toc]
+```
+
+@[toc]
+
+  </div>
+</div>
+
+# :pencil: Block Elements
+
+## Headers 見出し
+
+先頭に`#`をレベルの数だけ記述します。
+
+```
+# 見出し1
+## 見出し2
+### 見出し3
+#### 見出し4
+##### 見出し5
+###### 見出し6
+```
+
+### 見出し3
+#### 見出し4
+##### 見出し5
+###### 見出し6
+
+## Block 段落
+
+空白行を挟むことで段落となります。aaaa
+
+```
+段落1
+(空行)
+段落2
+```
+
+段落1
+
+段落2
+
+## Br 改行
+
+改行の前に半角スペース`  `を2つ記述します。  
+***この挙動は、オプションで変更可能です***
+
+```
+hoge
+fuga(スペース2つ)
+piyo
+```
+
+hoge
+fuga  
+piyo
+
+## Blockquotes 引用
+
+先頭に`>`を記述します。ネストは`>`を多重に記述します。
+
+```
+> 引用  
+> 引用
+>> 多重引用
+```
+
+> 引用  
+> 引用
+>> 多重引用
+
+## Code コード
+
+`` `バッククオート` `` 3つ、あるいはダッシュ`~`3つで囲みます。
+
+```
+print 'hoge'
+```
+
+### シンタックスハイライトとファイル名
+
+- [highlight.js Demo](https://highlightjs.org/static/demo/) の common カテゴリ内の言語に対応しています
+
+
+~~~
+```javascript:mersenne-twister.js
+function MersenneTwister(seed) {
+  if (arguments.length == 0) {
+    seed = new Date().getTime();
+  }
+
+  this._mt = new Array(624);
+  this.setSeed(seed);
+}
+```
+~~~
+
+```javascript:mersenne-twister.js
+function MersenneTwister(seed) {
+  if (arguments.length == 0) {
+    seed = new Date().getTime();
+  }
+
+  this._mt = new Array(624);
+  this.setSeed(seed);
+}
+```
+
+### インラインコード
+
+`` `バッククオート` `` で単語を囲むとインラインコードになります。
+
+```
+これは `インラインコード`です。
+```
+
+これは `インラインコード`です。
+
+## pre 整形済みテキスト
+
+半角スペース4個もしくはタブで、コードブロックをpre表示できます
+
+```
+    class Hoge
+        def hoge
+            print 'hoge'
+        end
+    end
+```
+
+    class Hoge
+        def hoge
+            print 'hoge'
+        end
+    end
+
+## Hr 水平線
+
+アンダースコア`_` 、アスタリスク`*`を3つ以上連続して記述します。
+
+```
+***
+___
+---
+```
+
+***
+___
+---
+
+
+
+# :pencil: Typography
+
+## 強調
+### em
+
+アスタリスク`*`もしくはアンダースコア`_`1個で文字列を囲みます。
+
+```
+これは *イタリック* です
+これは _イタリック_ です
+```
+
+これは *イタリック* です
+これは _イタリック_ です
+
+### strong
+
+アスタリスク`*`もしくはアンダースコア`_`2個で文字列を囲みます。
+
+```
+これは **ボールド** です
+これは __ボールド__ です
+```
+
+これは **ボールド** です
+これは __ボールド__ です
+
+### em + strong
+
+アスタリスク`*`もしくはアンダースコア`_`3個で文字列を囲みます。
+
+```
+これは ***イタリック&ボールド*** です
+これは ___イタリック&ボールド___ です
+```
+
+これは ***イタリック&ボールド*** です
+これは ___イタリック&ボールド___ です
+
+# :pencil: Images
+
+`![Alt文字列](URL)` で`<img>`タグを挿入できます。
+
+```markdown
+![Minion](https://octodex.github.com/images/minion.png)
+![Stormtroopocat](https://octodex.github.com/images/stormtroopocat.jpg "The Stormtroopocat")
+```
+
+![Minion](https://octodex.github.com/images/minion.png)
+![Stormtroopocat](https://octodex.github.com/images/stormtroopocat.jpg "The Stormtroopocat")
+
+画像の大きさなどの指定をする場合はimgタグを使用します。
+
+```html
+<img src="https://octodex.github.com/images/dojocat.jpg" width="200px">
+```
+
+<img src="https://octodex.github.com/images/dojocat.jpg" width="200px">
+
+
+# :pencil: Link
+
+## Markdown 標準
+
+`[表示テキスト](URL)`でリンクに変換されます。
+
+```
+[Google](https://www.google.co.jp/)
+```
+
+[Google](https://www.google.co.jp/)
+
+## Crowi 互換
+
+```
+[/Sandbox]
+</user/admin1>
+```
+
+[/Sandbox]  
+</user/admin1>
+
+## Pukiwiki like linker
+
+(available by [weseek/crowi-plugin-pukiwiki-like-linker
+](https://github.com/weseek/crowi-plugin-pukiwiki-like-linker) )
+
+最も柔軟な Linker です。  
+記述中のページを基点とした相対リンクと、表示テキストに対するリンクを同時に実現できます。
+
+```
+[[./Bootstrap3]]
+Bootstrap3のExampleは[[こちら>./Bootstrap3]]
+```
+
+[[../user]]  
+Bootstrap3のExampleは[[こちら>./Bootstrap3]]
+
+# :pencil: Lists
+
+## Ul 箇条書きリスト
+
+ハイフン`-`、プラス`+`、アスタリスク`*`のいずれかを先頭に記述します。  
+ネストはタブで表現します。
+
+```
+- リスト1
+    - リスト1_1
+        - リスト1_1_1
+        - リスト1_1_2
+    - リスト1_2
+- リスト2
+- リスト3
+```
+
+- リスト1
+    - リスト1_1
+        - リスト1_1_1
+        - リスト1_1_2
+    - リスト1_2
+- リスト2
+- リスト3
+
+## Ol 番号付きリスト
+
+`番号.`を先頭に記述します。ネストはタブで表現します。  
+番号は自動的に採番されるため、すべての行を1.と記述するのがお勧めです。
+
+```
+1. 番号付きリスト1
+    1. 番号付きリスト1-1
+    1. 番号付きリスト1-2
+1. 番号付きリスト2
+1. 番号付きリスト3
+```
+
+1. 番号付きリスト1
+    1. 番号付きリスト1-1
+    1. 番号付きリスト1-2
+1. 番号付きリスト2
+1. 番号付きリスト3
+
+
+## タスクリスト
+
+```
+- [ ] タスク 1
+    - [x] タスク 1.1
+    - [ ] タスク 1.2
+- [x] タスク2
+```
+
+- [ ] タスク 1
+    - [x] タスク 1.1
+    - [ ] タスク 1.2
+- [x] タスク2
+
+
+# :pencil: Table
+
+## Markdown 標準
+
+```markdown
+| Left align | Right align | Center align |
+|:-----------|------------:|:------------:|
+| This       | This        | This         |
+| column     | column      | column       |
+| will       | will        | will         |
+| be         | be          | be           |
+| left       | right       | center       |
+| aligned    | aligned     | aligned      |
+
+OR
+
+Left align | Right align | Center align
+:--|--:|:-:
+This       | This        | This
+column     | column      | column
+will       | will        | will
+be         | be          | be
+left       | right       | center
+aligned    | aligned     | aligned
+```
+
+| Left align | Right align | Center align |
+|:-----------|------------:|:------------:|
+| This       | This        | This         |
+| column     | column      | column       |
+| will       | will        | will         |
+| be         | be          | be           |
+| left       | right       | center       |
+| aligned    | aligned     | aligned      |
+
+## TSV (crowi-plus 独自記法)
+
+```
+::: tsv
+Content Cell 	Content Cell
+Content Cell 	Content Cell
+:::
+```
+
+::: tsv
+Content Cell	Content Cell
+Content Cell	Content Cell
+:::
+
+## TSV ヘッダ付き (crowi-plus 独自記法)
+
+```
+::: tsv-h
+First Header	Second Header
+Content Cell	Content Cell
+Content Cell	Content Cell
+:::
+```
+
+::: tsv-h
+First Header	Second Header
+Content Cell	Content Cell
+Content Cell	Content Cell
+:::
+
+## CSV (crowi-plus 独自記法)
+
+```
+::: csv
+Content Cell,Content Cell
+Content Cell,Content Cell
+:::
+```
+
+::: csv
+Content Cell,Content Cell
+Content Cell,Content Cell
+:::
+
+## CSV ヘッダ付き (crowi-plus 独自記法)
+
+```
+::: csv-h
+First Header,Second Header
+Content Cell,Content Cell
+Content Cell,Content Cell
+:::
+```
+
+::: csv-h
+First Header,Second Header
+Content Cell,Content Cell
+Content Cell,Content Cell
+:::
+
+
+# :pencil: Footnote
+
+脚注への参照[^1]を書くことができます。また、インラインの脚注^[インラインで記述できる脚注です]を入れる事も出来ます。
+
+長い脚注は[^longnote]のように書くことができます。
+
+[^1]: 1つめの脚注への参照です。
+
+[^longnote]: 脚注を複数ブロックで書く例です。
+
+    後続の段落はインデントされて、前の脚注に属します。
+
+
+# :pencil: Emoji
+
+See [emojione](https://www.emojione.com/)
+
+:smiley: :smile: :laughing: :innocent: :drooling_face:
+
+:family: :family_man_boy: :family_man_girl: :family_man_girl_girl: :family_woman_girl_girl:
+
+:thumbsup: :thumbsdown: :open_hands: :raised_hands: :point_right:
+
+:apple: :green_apple: :strawberry: :cake: :hamburger:
+
+:basketball: :football: :baseball: :volleyball: :8ball:
+
+:hearts: :broken_heart: :heartbeat: :heartpulse: :heart_decoration:
+
+:watch: :gear: :gem: :wrench: :envelope:
+
+
+# :pencil: Math
+
+See [MathJax](https://www.mathjax.org/).
+
+## Inline Formula
+
+When $a \ne 0$, there are two solutions to \(ax^2 + bx + c = 0\) and they are
+  $$x = {-b \pm \sqrt{b^2-4ac} \over 2a}.$$
+
+## The Lorenz Equations
+
+$$
+\begin{align}
+\dot{x} & = \sigma(y-x) \\
+\dot{y} & = \rho x - y - xz \\
+\dot{z} & = -\beta z + xy
+\end{align}
+$$
+
+
+## The Cauchy-Schwarz Inequality
+
+$$
+\left( \sum_{k=1}^n a_k b_k \right)^{\!\!2} \leq
+ \left( \sum_{k=1}^n a_k^2 \right) \left( \sum_{k=1}^n b_k^2 \right)
+$$
+
+## A Cross Product Formula
+
+$$
+\mathbf{V}_1 \times \mathbf{V}_2 =
+ \begin{vmatrix}
+  \mathbf{i} & \mathbf{j} & \mathbf{k} \\
+  \frac{\partial X}{\partial u} & \frac{\partial Y}{\partial u} & 0 \\
+  \frac{\partial X}{\partial v} & \frac{\partial Y}{\partial v} & 0 \\
+ \end{vmatrix}
+$$
+
+
+## The probability of getting $\left(k\right)$ heads when flipping $\left(n\right)$ coins is:
+
+$$
+P(E) = {n \choose k} p^k (1-p)^{ n-k}
+$$
+
+## An Identity of Ramanujan
+
+$$
+\frac{1}{(\sqrt{\phi \sqrt{5}}-\phi) e^{\frac25 \pi}} =
+     1+\frac{e^{-2\pi}} {1+\frac{e^{-4\pi}} {1+\frac{e^{-6\pi}}
+      {1+\frac{e^{-8\pi}} {1+\ldots} } } }
+$$
+
+## A Rogers-Ramanujan Identity
+
+$$
+1 +  \frac{q^2}{(1-q)}+\frac{q^6}{(1-q)(1-q^2)}+\cdots =
+    \prod_{j=0}^{\infty}\frac{1}{(1-q^{5j+2})(1-q^{5j+3})},
+     \quad\quad \text{for $|q|<1$}.
+$$
+
+## Maxwell's Equations
+
+$$
+\begin{align}
+  \nabla \times \vec{\mathbf{B}} -\, \frac1c\, \frac{\partial\vec{\mathbf{E}}}{\partial t} & = \frac{4\pi}{c}\vec{\mathbf{j}} \\
+  \nabla \cdot \vec{\mathbf{E}} & = 4 \pi \rho \\
+  \nabla \times \vec{\mathbf{E}}\, +\, \frac1c\, \frac{\partial\vec{\mathbf{B}}}{\partial t} & = \vec{\mathbf{0}} \\
+  \nabla \cdot \vec{\mathbf{B}} & = 0
+\end{align}
+$$
+
+<!-- Reset MathJax -->
+<div class="clearfix"></div>
+
+
+# :pencil: UML Diagrams
+
+See [PlantUML](http://plantuml.com/).
+
+## シーケンス図
+
+@startuml
+skinparam sequenceArrowThickness 2
+skinparam roundcorner 20
+skinparam maxmessagesize 60
+skinparam sequenceParticipant underline
+
+actor User
+participant "First Class" as A
+participant "Second Class" as B
+participant "Last Class" as C
+
+User -> A: DoWork
+activate A
+
+A -> B: Create Request
+activate B
+
+B -> C: DoWork
+activate C
+C --> B: WorkDone
+destroy C
+
+B --> A: Request Created
+deactivate B
+
+A --> User: Done
+deactivate A
+
+@enduml
+
+<!-- Reset PlantUML -->
+<div class="clearfix"></div>
+
+
+## クラス図
+
+@startuml
+
+class BaseClass
+
+namespace net.dummy #DDDDDD {
+    .BaseClass <|-- Person
+    Meeting o-- Person
+    
+    .BaseClass <|- Meeting
+}
+
+namespace net.foo {
+  net.dummy.Person  <|- Person
+  .BaseClass <|-- Person
+
+  net.dummy.Meeting o-- Person
+}
+
+BaseClass <|-- net.unused.Person
+
+@enduml
+
+<!-- Reset PlantUML -->
+<div class="clearfix"></div>
+
+
+## コンポーネント図
+
+@startuml
+
+package "Some Group" {
+  HTTP - [First Component]
+  [Another Component]
+}
+ 
+node "Other Groups" {
+  FTP - [Second Component]
+  [First Component] --> FTP
+} 
+
+cloud {
+  [Example 1]
+}
+
+
+database "MySql" {
+  folder "This is my folder" {
+    [Folder 3]
+  }
+  frame "Foo" {
+    [Frame 4]
+  }
+}
+
+
+[Another Component] --> [Example 1]
+[Example 1] --> [Folder 3]
+[Folder 3] --> [Frame 4]
+
+@enduml
+
+<!-- Reset PlantUML -->
+<div class="clearfix"></div>
+
+
+## ステート図
+
+
+@startuml
+scale 600 width
+
+[*] -> State1
+State1 --> State2 : Succeeded
+State1 --> [*] : Aborted
+State2 --> State3 : Succeeded
+State2 --> [*] : Aborted
+state State3 {
+  state "Accumulate Enough Data\nLong State Name" as long1
+  long1 : Just a test
+  [*] --> long1
+  long1 --> long1 : New Data
+  long1 --> ProcessData : Enough Data
+}
+State3 --> State3 : Failed
+State3 --> [*] : Succeeded / Save Result
+State3 --> [*] : Aborted
+ 
+@enduml
+
+<!-- Reset PlantUML -->
+<div class="clearfix"></div>
+
+

+ 13 - 6
lib/locales/en-US/translation.json

@@ -47,6 +47,7 @@
   "View diff": "View diff",
   "View diff": "View diff",
 
 
   "User ID": "User ID",
   "User ID": "User ID",
+  "Home": "Home",
   "User Settings": "User Settings",
   "User Settings": "User Settings",
   "User Information": "User Information",
   "User Information": "User Information",
   "Basic Info": "Basic Info",
   "Basic Info": "Basic Info",
@@ -65,6 +66,11 @@
   "Google Setting": "Google Setting",
   "Google Setting": "Google Setting",
   "Connected": "Connected",
   "Connected": "Connected",
   "Disconnect": "Disconnect",
   "Disconnect": "Disconnect",
+  "Show": "Show",
+  "Hide": "Hide",
+  "Disclose E-mail": "Disclose E-mail",
+  
+  
 
 
   "Create today's": "Create today's ...",
   "Create today's": "Create today's ...",
   "Memo": "memo",
   "Memo": "memo",
@@ -79,6 +85,7 @@
   "Anyone with the link": "Anyone with the link",
   "Anyone with the link": "Anyone with the link",
   "Specified users only": "Specified users only",
   "Specified users only": "Specified users only",
   "Just me": "Just me",
   "Just me": "Just me",
+  "Only inside the group": "Only inside the group",
   "Shareable link": "Shareable link",
   "Shareable link": "Shareable link",
 
 
   "Show latest": "Show latest",
   "Show latest": "Show latest",
@@ -129,7 +136,6 @@
   "New password": "New password",
   "New password": "New password",
   "Re-enter new password": "Re-enter new password",
   "Re-enter new password": "Re-enter new password",
   "Password is not set": "Password is not set",
   "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 Settings": "API Settings",
   "API Token Settings": "API Token Settings",
   "API Token Settings": "API Token Settings",
@@ -143,6 +149,7 @@
       "notice": {
       "notice": {
           "version": "This is not the current version.",
           "version": "This is not the current version.",
           "moved": "This page was moved from <code>%s</code>",
           "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.",
           "unlinked": "Redirect pages to this page have been deleted.",
           "restricted": "Access to this page is restricted"
           "restricted": "Access to this page is restricted"
       }
       }
@@ -224,8 +231,8 @@
   },
   },
 
 
   "app_setting": {
   "app_setting": {
-    "Wiki name": "Wiki name",
-    "wiki_change": "You can change Wiki name which is used for header and HTML title.",
+    "Site Name": "Site name",
+    "sitename_change": "You can change Site Name which is used for header and HTML title.",
     "header_content": "The contents entered here will be shown in the header etc.",
     "header_content": "The contents entered here will be shown in the header etc.",
     "Confidential name": "Confidential name",
     "Confidential name": "Confidential name",
     "ex): internal use only":"ex): internal use only",
     "ex): internal use only":"ex): internal use only",
@@ -250,8 +257,8 @@
     "Plugin settings": "Plugin settings",
     "Plugin settings": "Plugin settings",
     "Enable plugin loading": "Enable plugin loading",
     "Enable plugin loading": "Enable plugin loading",
     "Load plugins": "Load plugins",
     "Load plugins": "Load plugins",
-    "valid": "Valid",
-    "invalid": "Invalid"
+    "Enable": "Enable",
+    "Disable": "Disable"
 
 
   },
   },
   "security_setting": {
   "security_setting": {
@@ -265,7 +272,7 @@
     "without_encryption": "Please be noted that your ID and Password will be sent wihtout encryption.",
     "without_encryption": "Please be noted that your ID and Password will be sent wihtout encryption.",
     "users_without_account": "Users without account is not accessible",
     "users_without_account": "Users without account is not accessible",
     "restrict_emails": "You can restrict registerable e-mail address.",
     "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.",
     "only_those":" Only those whose e-mail address including the company address can register.",
     "insert_single":"Please insert single e-mail address per line.",
     "insert_single":"Please insert single e-mail address per line.",
     "Authentication mechanism settings":"Authentication mechanism settings"
     "Authentication mechanism settings":"Authentication mechanism settings"

+ 29 - 0
lib/locales/en-US/welcome.md

@@ -0,0 +1,29 @@
+# Welcome to GROWI :anchor:
+
+[![Github Releases](https://img.shields.io/github/release/weseek/growi.svg)](https://github.com/weseek/growi/releases/latest)
+[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE)
+
+<div class="panel panel-default">
+  <div class="panel-heading">Tips</div>
+  <div class="panel-body"><ul>
+    <li>Ctrl(⌘)-/ でショートカットヘルプを表示します</li>
+      <li>HTML/CSS の記述時は、<a href="https://getbootstrap.com/docs/3.3/css/">Bootstrap 3</a> を利用できます</li>
+  </ul></div>
+</div>
+
+<div class="clearfix"></div>
+
+Contents
+=========
+
+|All Pages|[/Sandbox]|
+| --- | --- |
+| $lsx(/) | <div class="alert alert-success"><span style="font-size: x-large;"><i class="icon-check"></i> [[Sandboxをチェック>/Sandbox]]</span></div> $lsx(/Sandbox)|
+
+Slack
+=====
+
+<a href="https://growi-slackin.weseek.co.jp/"><img src="https://growi-slackin.weseek.co.jp/badge.svg"></a>
+
+GROWI をより良いものにするために、是非 Slack に参加してください。  
+開発に関する議論を行っている他、導入時の質問等も受け付けています。

+ 655 - 0
lib/locales/ja/sandbox.md

@@ -0,0 +1,655 @@
+<div class="panel panel-default">
+  <div class="panel-body">
+
+# 目次
+
+```
+@[toc]
+```
+
+@[toc]
+
+  </div>
+</div>
+
+# :pencil: Block Elements
+
+## Headers 見出し
+
+先頭に`#`をレベルの数だけ記述します。
+
+```
+# 見出し1
+## 見出し2
+### 見出し3
+#### 見出し4
+##### 見出し5
+###### 見出し6
+```
+
+### 見出し3
+#### 見出し4
+##### 見出し5
+###### 見出し6
+
+## Block 段落
+
+空白行を挟むことで段落となります。aaaa
+
+```
+段落1
+(空行)
+段落2
+```
+
+段落1
+
+段落2
+
+## Br 改行
+
+改行の前に半角スペース`  `を2つ記述します。  
+***この挙動は、オプションで変更可能です***
+
+```
+hoge
+fuga(スペース2つ)
+piyo
+```
+
+hoge
+fuga  
+piyo
+
+## Blockquotes 引用
+
+先頭に`>`を記述します。ネストは`>`を多重に記述します。
+
+```
+> 引用  
+> 引用
+>> 多重引用
+```
+
+> 引用  
+> 引用
+>> 多重引用
+
+## Code コード
+
+`` `バッククオート` `` 3つ、あるいはダッシュ`~`3つで囲みます。
+
+```
+print 'hoge'
+```
+
+### シンタックスハイライトとファイル名
+
+- [highlight.js Demo](https://highlightjs.org/static/demo/) の common カテゴリ内の言語に対応しています
+
+
+~~~
+```javascript:mersenne-twister.js
+function MersenneTwister(seed) {
+  if (arguments.length == 0) {
+    seed = new Date().getTime();
+  }
+
+  this._mt = new Array(624);
+  this.setSeed(seed);
+}
+```
+~~~
+
+```javascript:mersenne-twister.js
+function MersenneTwister(seed) {
+  if (arguments.length == 0) {
+    seed = new Date().getTime();
+  }
+
+  this._mt = new Array(624);
+  this.setSeed(seed);
+}
+```
+
+### インラインコード
+
+`` `バッククオート` `` で単語を囲むとインラインコードになります。
+
+```
+これは `インラインコード`です。
+```
+
+これは `インラインコード`です。
+
+## pre 整形済みテキスト
+
+半角スペース4個もしくはタブで、コードブロックをpre表示できます
+
+```
+    class Hoge
+        def hoge
+            print 'hoge'
+        end
+    end
+```
+
+    class Hoge
+        def hoge
+            print 'hoge'
+        end
+    end
+
+## Hr 水平線
+
+アンダースコア`_` 、アスタリスク`*`を3つ以上連続して記述します。
+
+```
+***
+___
+---
+```
+
+***
+___
+---
+
+
+
+# :pencil: Typography
+
+## 強調
+### em
+
+アスタリスク`*`もしくはアンダースコア`_`1個で文字列を囲みます。
+
+```
+これは *イタリック* です
+これは _イタリック_ です
+```
+
+これは *イタリック* です
+これは _イタリック_ です
+
+### strong
+
+アスタリスク`*`もしくはアンダースコア`_`2個で文字列を囲みます。
+
+```
+これは **ボールド** です
+これは __ボールド__ です
+```
+
+これは **ボールド** です
+これは __ボールド__ です
+
+### em + strong
+
+アスタリスク`*`もしくはアンダースコア`_`3個で文字列を囲みます。
+
+```
+これは ***イタリック&ボールド*** です
+これは ___イタリック&ボールド___ です
+```
+
+これは ***イタリック&ボールド*** です
+これは ___イタリック&ボールド___ です
+
+# :pencil: Images
+
+`![Alt文字列](URL)` で`<img>`タグを挿入できます。
+
+```markdown
+![Minion](https://octodex.github.com/images/minion.png)
+![Stormtroopocat](https://octodex.github.com/images/stormtroopocat.jpg "The Stormtroopocat")
+```
+
+![Minion](https://octodex.github.com/images/minion.png)
+![Stormtroopocat](https://octodex.github.com/images/stormtroopocat.jpg "The Stormtroopocat")
+
+画像の大きさなどの指定をする場合はimgタグを使用します。
+
+```html
+<img src="https://octodex.github.com/images/dojocat.jpg" width="200px">
+```
+
+<img src="https://octodex.github.com/images/dojocat.jpg" width="200px">
+
+
+# :pencil: Link
+
+## Markdown 標準
+
+`[表示テキスト](URL)`でリンクに変換されます。
+
+```
+[Google](https://www.google.co.jp/)
+```
+
+[Google](https://www.google.co.jp/)
+
+## Crowi 互換
+
+```
+[/Sandbox]
+</user/admin1>
+```
+
+[/Sandbox]  
+</user/admin1>
+
+## Pukiwiki like linker
+
+(available by [weseek/crowi-plugin-pukiwiki-like-linker
+](https://github.com/weseek/crowi-plugin-pukiwiki-like-linker) )
+
+最も柔軟な Linker です。  
+記述中のページを基点とした相対リンクと、表示テキストに対するリンクを同時に実現できます。
+
+```
+[[./Bootstrap3]]
+Bootstrap3のExampleは[[こちら>./Bootstrap3]]
+```
+
+[[../user]]  
+Bootstrap3のExampleは[[こちら>./Bootstrap3]]
+
+# :pencil: Lists
+
+## Ul 箇条書きリスト
+
+ハイフン`-`、プラス`+`、アスタリスク`*`のいずれかを先頭に記述します。  
+ネストはタブで表現します。
+
+```
+- リスト1
+    - リスト1_1
+        - リスト1_1_1
+        - リスト1_1_2
+    - リスト1_2
+- リスト2
+- リスト3
+```
+
+- リスト1
+    - リスト1_1
+        - リスト1_1_1
+        - リスト1_1_2
+    - リスト1_2
+- リスト2
+- リスト3
+
+## Ol 番号付きリスト
+
+`番号.`を先頭に記述します。ネストはタブで表現します。  
+番号は自動的に採番されるため、すべての行を1.と記述するのがお勧めです。
+
+```
+1. 番号付きリスト1
+    1. 番号付きリスト1-1
+    1. 番号付きリスト1-2
+1. 番号付きリスト2
+1. 番号付きリスト3
+```
+
+1. 番号付きリスト1
+    1. 番号付きリスト1-1
+    1. 番号付きリスト1-2
+1. 番号付きリスト2
+1. 番号付きリスト3
+
+
+## タスクリスト
+
+```
+- [ ] タスク 1
+    - [x] タスク 1.1
+    - [ ] タスク 1.2
+- [x] タスク2
+```
+
+- [ ] タスク 1
+    - [x] タスク 1.1
+    - [ ] タスク 1.2
+- [x] タスク2
+
+
+# :pencil: Table
+
+## Markdown 標準
+
+```markdown
+| Left align | Right align | Center align |
+|:-----------|------------:|:------------:|
+| This       | This        | This         |
+| column     | column      | column       |
+| will       | will        | will         |
+| be         | be          | be           |
+| left       | right       | center       |
+| aligned    | aligned     | aligned      |
+
+OR
+
+Left align | Right align | Center align
+:--|--:|:-:
+This       | This        | This
+column     | column      | column
+will       | will        | will
+be         | be          | be
+left       | right       | center
+aligned    | aligned     | aligned
+```
+
+| Left align | Right align | Center align |
+|:-----------|------------:|:------------:|
+| This       | This        | This         |
+| column     | column      | column       |
+| will       | will        | will         |
+| be         | be          | be           |
+| left       | right       | center       |
+| aligned    | aligned     | aligned      |
+
+## TSV (crowi-plus 独自記法)
+
+```
+::: tsv
+Content Cell 	Content Cell
+Content Cell 	Content Cell
+:::
+```
+
+::: tsv
+Content Cell	Content Cell
+Content Cell	Content Cell
+:::
+
+## TSV ヘッダ付き (crowi-plus 独自記法)
+
+```
+::: tsv-h
+First Header	Second Header
+Content Cell	Content Cell
+Content Cell	Content Cell
+:::
+```
+
+::: tsv-h
+First Header	Second Header
+Content Cell	Content Cell
+Content Cell	Content Cell
+:::
+
+## CSV (crowi-plus 独自記法)
+
+```
+::: csv
+Content Cell,Content Cell
+Content Cell,Content Cell
+:::
+```
+
+::: csv
+Content Cell,Content Cell
+Content Cell,Content Cell
+:::
+
+## CSV ヘッダ付き (crowi-plus 独自記法)
+
+```
+::: csv-h
+First Header,Second Header
+Content Cell,Content Cell
+Content Cell,Content Cell
+:::
+```
+
+::: csv-h
+First Header,Second Header
+Content Cell,Content Cell
+Content Cell,Content Cell
+:::
+
+
+# :pencil: Footnote
+
+脚注への参照[^1]を書くことができます。また、インラインの脚注^[インラインで記述できる脚注です]を入れる事も出来ます。
+
+長い脚注は[^longnote]のように書くことができます。
+
+[^1]: 1つめの脚注への参照です。
+
+[^longnote]: 脚注を複数ブロックで書く例です。
+
+    後続の段落はインデントされて、前の脚注に属します。
+
+
+# :pencil: Emoji
+
+See [emojione](https://www.emojione.com/)
+
+:smiley: :smile: :laughing: :innocent: :drooling_face:
+
+:family: :family_man_boy: :family_man_girl: :family_man_girl_girl: :family_woman_girl_girl:
+
+:thumbsup: :thumbsdown: :open_hands: :raised_hands: :point_right:
+
+:apple: :green_apple: :strawberry: :cake: :hamburger:
+
+:basketball: :football: :baseball: :volleyball: :8ball:
+
+:hearts: :broken_heart: :heartbeat: :heartpulse: :heart_decoration:
+
+:watch: :gear: :gem: :wrench: :envelope:
+
+
+# :pencil: Math
+
+See [MathJax](https://www.mathjax.org/).
+
+## Inline Formula
+
+When $a \ne 0$, there are two solutions to \(ax^2 + bx + c = 0\) and they are
+  $$x = {-b \pm \sqrt{b^2-4ac} \over 2a}.$$
+
+## The Lorenz Equations
+
+$$
+\begin{align}
+\dot{x} & = \sigma(y-x) \\
+\dot{y} & = \rho x - y - xz \\
+\dot{z} & = -\beta z + xy
+\end{align}
+$$
+
+
+## The Cauchy-Schwarz Inequality
+
+$$
+\left( \sum_{k=1}^n a_k b_k \right)^{\!\!2} \leq
+ \left( \sum_{k=1}^n a_k^2 \right) \left( \sum_{k=1}^n b_k^2 \right)
+$$
+
+## A Cross Product Formula
+
+$$
+\mathbf{V}_1 \times \mathbf{V}_2 =
+ \begin{vmatrix}
+  \mathbf{i} & \mathbf{j} & \mathbf{k} \\
+  \frac{\partial X}{\partial u} & \frac{\partial Y}{\partial u} & 0 \\
+  \frac{\partial X}{\partial v} & \frac{\partial Y}{\partial v} & 0 \\
+ \end{vmatrix}
+$$
+
+
+## The probability of getting $\left(k\right)$ heads when flipping $\left(n\right)$ coins is:
+
+$$
+P(E) = {n \choose k} p^k (1-p)^{ n-k}
+$$
+
+## An Identity of Ramanujan
+
+$$
+\frac{1}{(\sqrt{\phi \sqrt{5}}-\phi) e^{\frac25 \pi}} =
+     1+\frac{e^{-2\pi}} {1+\frac{e^{-4\pi}} {1+\frac{e^{-6\pi}}
+      {1+\frac{e^{-8\pi}} {1+\ldots} } } }
+$$
+
+## A Rogers-Ramanujan Identity
+
+$$
+1 +  \frac{q^2}{(1-q)}+\frac{q^6}{(1-q)(1-q^2)}+\cdots =
+    \prod_{j=0}^{\infty}\frac{1}{(1-q^{5j+2})(1-q^{5j+3})},
+     \quad\quad \text{for $|q|<1$}.
+$$
+
+## Maxwell's Equations
+
+$$
+\begin{align}
+  \nabla \times \vec{\mathbf{B}} -\, \frac1c\, \frac{\partial\vec{\mathbf{E}}}{\partial t} & = \frac{4\pi}{c}\vec{\mathbf{j}} \\
+  \nabla \cdot \vec{\mathbf{E}} & = 4 \pi \rho \\
+  \nabla \times \vec{\mathbf{E}}\, +\, \frac1c\, \frac{\partial\vec{\mathbf{B}}}{\partial t} & = \vec{\mathbf{0}} \\
+  \nabla \cdot \vec{\mathbf{B}} & = 0
+\end{align}
+$$
+
+<!-- Reset MathJax -->
+<div class="clearfix"></div>
+
+
+# :pencil: UML Diagrams
+
+See [PlantUML](http://plantuml.com/).
+
+## シーケンス図
+
+@startuml
+skinparam sequenceArrowThickness 2
+skinparam roundcorner 20
+skinparam maxmessagesize 60
+skinparam sequenceParticipant underline
+
+actor User
+participant "First Class" as A
+participant "Second Class" as B
+participant "Last Class" as C
+
+User -> A: DoWork
+activate A
+
+A -> B: Create Request
+activate B
+
+B -> C: DoWork
+activate C
+C --> B: WorkDone
+destroy C
+
+B --> A: Request Created
+deactivate B
+
+A --> User: Done
+deactivate A
+
+@enduml
+
+<!-- Reset PlantUML -->
+<div class="clearfix"></div>
+
+
+## クラス図
+
+@startuml
+
+class BaseClass
+
+namespace net.dummy #DDDDDD {
+    .BaseClass <|-- Person
+    Meeting o-- Person
+    
+    .BaseClass <|- Meeting
+}
+
+namespace net.foo {
+  net.dummy.Person  <|- Person
+  .BaseClass <|-- Person
+
+  net.dummy.Meeting o-- Person
+}
+
+BaseClass <|-- net.unused.Person
+
+@enduml
+
+<!-- Reset PlantUML -->
+<div class="clearfix"></div>
+
+
+## コンポーネント図
+
+@startuml
+
+package "Some Group" {
+  HTTP - [First Component]
+  [Another Component]
+}
+ 
+node "Other Groups" {
+  FTP - [Second Component]
+  [First Component] --> FTP
+} 
+
+cloud {
+  [Example 1]
+}
+
+
+database "MySql" {
+  folder "This is my folder" {
+    [Folder 3]
+  }
+  frame "Foo" {
+    [Frame 4]
+  }
+}
+
+
+[Another Component] --> [Example 1]
+[Example 1] --> [Folder 3]
+[Folder 3] --> [Frame 4]
+
+@enduml
+
+<!-- Reset PlantUML -->
+<div class="clearfix"></div>
+
+
+## ステート図
+
+
+@startuml
+scale 600 width
+
+[*] -> State1
+State1 --> State2 : Succeeded
+State1 --> [*] : Aborted
+State2 --> State3 : Succeeded
+State2 --> [*] : Aborted
+state State3 {
+  state "Accumulate Enough Data\nLong State Name" as long1
+  long1 : Just a test
+  [*] --> long1
+  long1 --> long1 : New Data
+  long1 --> ProcessData : Enough Data
+}
+State3 --> State3 : Failed
+State3 --> [*] : Succeeded / Save Result
+State3 --> [*] : Aborted
+ 
+@enduml
+
+<!-- Reset PlantUML -->
+<div class="clearfix"></div>
+
+

+ 16 - 7
lib/locales/ja/translation.json

@@ -44,6 +44,7 @@
   "View diff": "差分を見る",
   "View diff": "差分を見る",
 
 
   "User ID": "ユーザーID",
   "User ID": "ユーザーID",
+  "Home": "ホーム",
   "User Settings": "ユーザー設定",
   "User Settings": "ユーザー設定",
   "User Information": "ユーザー情報",
   "User Information": "ユーザー情報",
   "Basic Info": "ユーザーの基本情報",
   "Basic Info": "ユーザーの基本情報",
@@ -62,6 +63,12 @@
   "Google Setting": "Google設定",
   "Google Setting": "Google設定",
   "Connected": "接続されています",
   "Connected": "接続されています",
   "Disconnect": "接続を解除",
   "Disconnect": "接続を解除",
+  "Show": "公開",
+  "Hide": "非公開",
+  "Disclose E-mail": "メールアドレスの公開",
+  
+ 
+  
 
 
   "Create today's": "今日の◯◯を作成",
   "Create today's": "今日の◯◯を作成",
   "Memo": "メモ",
   "Memo": "メモ",
@@ -76,14 +83,15 @@
   "Table of Contents": "目次",
   "Table of Contents": "目次",
   "Management Wiki Home": "Wiki管理トップ",
   "Management Wiki Home": "Wiki管理トップ",
   "App settings": "アプリ設定",
   "App settings": "アプリ設定",
-  "Security settings": "セキュリティ設定",
   "Markdown settings": "Markdown設定",
   "Markdown settings": "Markdown設定",
   "Customize": "カスタマイズ",
   "Customize": "カスタマイズ",
   "Notification settings": "通知設定",
   "Notification settings": "通知設定",
   "User management": "ユーザー管理",
   "User management": "ユーザー管理",
+  "External Account management": "外部アカウント管理",
+  "UserGroup management": "グループ管理",
+  "Full Text Search management": "全文検索管理",
   "Basic settings": "基本設定",
   "Basic settings": "基本設定",
   "Basic authentication": "Basic認証",
   "Basic authentication": "Basic認証",
-  "Password": "パスワード",
   "Guest users access": "ゲストユーザーのアクセス",
   "Guest users access": "ゲストユーザーのアクセス",
   "Register limitation": "登録の制限",
   "Register limitation": "登録の制限",
   "The contents entered here will be shown in the header etc": "ここに入力した内容は、ヘッダー等に表示されます。",
   "The contents entered here will be shown in the header etc": "ここに入力した内容は、ヘッダー等に表示されます。",
@@ -91,6 +99,7 @@
   "Anyone with the link": "リンクを知っている人のみ",
   "Anyone with the link": "リンクを知っている人のみ",
   "Specified users": "特定ユーザーのみ",
   "Specified users": "特定ユーザーのみ",
   "Just me": "自分のみ",
   "Just me": "自分のみ",
+  "Only inside the group": "特定グループのみ",
   "Shareable link": "このページの共有用URL",
   "Shareable link": "このページの共有用URL",
   "The whitelist of registration permission E-mail address": "登録許可メールアドレスの<br>ホワイトリスト",
   "The whitelist of registration permission E-mail address": "登録許可メールアドレスの<br>ホワイトリスト",
   "Selecting authentication mechanism": "認証機構選択",
   "Selecting authentication mechanism": "認証機構選択",
@@ -145,7 +154,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": "<code>%s</code> と設定されたパスワードの組み合わせでログイン可能になります。",
 
 
   "Security settings": "セキュリティ設定",
   "Security settings": "セキュリティ設定",
 
 
@@ -158,6 +166,7 @@
       "notice": {
       "notice": {
           "version": "これは現在の版ではありません。",
           "version": "これは現在の版ではありません。",
           "moved": "このページは <code>%s</code> から移動しました。",
           "moved": "このページは <code>%s</code> から移動しました。",
+          "duplicated": "このページは <code>%s</code> から複製されました。",
           "unlinked": "このページへのリダイレクトは削除されました。",
           "unlinked": "このページへのリダイレクトは削除されました。",
           "restricted": "このページの閲覧は制限されています"
           "restricted": "このページの閲覧は制限されています"
       }
       }
@@ -241,8 +250,8 @@
   },
   },
 
 
   "app_setting": {
   "app_setting": {
-    "Wiki name": "Wikiの名前",
-    "wiki_change": "ヘッダーやHTMLタイトルに使用されるWikiの名前を変更できます。",
+    "Site Name": "サイト名",
+    "sitename_change": "ヘッダーや HTML タイトルに使用されるサイト名を変更できます。",
     "header_content": "ここに入力した内容は、ヘッダー等に表示されます。",
     "header_content": "ここに入力した内容は、ヘッダー等に表示されます。",
     "Confidential name": "コンフィデンシャル表示",
     "Confidential name": "コンフィデンシャル表示",
     "ex): internal use only": "例: 社外秘",
     "ex): internal use only": "例: 社外秘",
@@ -267,8 +276,8 @@
     "Plugin settings": "プラグイン設定",
     "Plugin settings": "プラグイン設定",
     "Enable plugin loading": "プラグインの読み込みを有効にします。",
     "Enable plugin loading": "プラグインの読み込みを有効にします。",
     "Load plugins": "プラグインを読み込む",
     "Load plugins": "プラグインを読み込む",
-    "valid": "有効",
-    "invalid": "無効"
+    "Enable": "有効",
+    "Disable": "無効"
 
 
    },
    },
 
 

+ 29 - 0
lib/locales/ja/welcome.md

@@ -0,0 +1,29 @@
+# Welcome to GROWI :anchor:
+
+[![Github Releases](https://img.shields.io/github/release/weseek/growi.svg)](https://github.com/weseek/growi/releases/latest)
+[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE)
+
+<div class="panel panel-default">
+  <div class="panel-heading">Tips</div>
+  <div class="panel-body"><ul>
+    <li>Ctrl(⌘)-/ でショートカットヘルプを表示します</li>
+      <li>HTML/CSS の記述時は、<a href="https://getbootstrap.com/docs/3.3/css/">Bootstrap 3</a> を利用できます</li>
+  </ul></div>
+</div>
+
+<div class="clearfix"></div>
+
+Contents
+=========
+
+|All Pages|[/Sandbox]|
+| --- | --- |
+| $lsx(/) | <div class="alert alert-success"><span style="font-size: x-large;"><i class="icon-check"></i> [[Sandboxをチェック>/Sandbox]]</span></div> $lsx(/Sandbox)|
+
+Slack
+=====
+
+<a href="https://growi-slackin.weseek.co.jp/"><img src="https://growi-slackin.weseek.co.jp/badge.svg"></a>
+
+GROWI をより良いものにするために、是非 Slack に参加してください。  
+開発に関する議論を行っている他、導入時の質問等も受け付けています。

+ 34 - 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() {
   function getArrayForInstalling() {
     let config = getDefaultCrowiConfigs();
     let config = getDefaultCrowiConfigs();
 
 
     // overwrite
     // overwrite
-    config['app:title'] = 'crowi-plus';
     config['app:fileUpload'] = true;
     config['app:fileUpload'] = true;
     config['security:isEnabledPassport'] = 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;
     config['customize:isSavedStatesOfTabChanges'] = false;
 
 
     return config;
     return config;
@@ -44,7 +43,6 @@ module.exports = function(crowi) {
   {
   {
     return {
     return {
       //'app:installed'     : "0.0.0",
       //'app:installed'     : "0.0.0",
-      'app:title'         : 'Crowi',
       'app:confidential'  : '',
       'app:confidential'  : '',
 
 
       'app:fileUpload'    : false,
       'app:fileUpload'    : false,
@@ -66,7 +64,7 @@ module.exports = function(crowi) {
       'security:passport-ldap:groupSearchFilter' : undefined,
       'security:passport-ldap:groupSearchFilter' : undefined,
       'security:passport-ldap:groupDnProperty' : undefined,
       'security:passport-ldap:groupDnProperty' : undefined,
 
 
-      'aws:bucket'          : 'crowi',
+      'aws:bucket'          : 'growi',
       'aws:region'          : 'ap-northeast-1',
       'aws:region'          : 'ap-northeast-1',
       'aws:accessKeyId'     : '',
       'aws:accessKeyId'     : '',
       'aws:secretAccessKey' : '',
       'aws:secretAccessKey' : '',
@@ -85,8 +83,10 @@ module.exports = function(crowi) {
       'customize:css' : '',
       'customize:css' : '',
       'customize:script' : '',
       'customize:script' : '',
       'customize:header' : '',
       'customize:header' : '',
+      'customize:title' : '',
       'customize:highlightJsStyle' : 'github',
       'customize:highlightJsStyle' : 'github',
       'customize:highlightJsStyleBorder' : false,
       'customize:highlightJsStyleBorder' : false,
+      'customize:theme' : 'default',
       'customize:behavior' : 'crowi',
       'customize:behavior' : 'crowi',
       'customize:layout' : 'crowi',
       'customize:layout' : 'crowi',
       'customize:isEnabledTimeline' : true,
       '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)
   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) {
     if (Object.keys(config.crowi).length == 0) {
       return true;
       return true;
     }
     }
@@ -360,6 +366,26 @@ module.exports = function(crowi) {
     return getValueForCrowiNS(config, key);
     return getValueForCrowiNS(config, key);
   }
   }
 
 
+  configSchema.statics.theme = function(config)
+  {
+    const key = 'customize:theme';
+    return getValueForCrowiNS(config, key);
+  }
+
+  configSchema.statics.customTitle = function(config, page)
+  {
+    const key = 'customize:title';
+    let customTitle = getValueForCrowiNS(config, key);
+
+    if (customTitle == null || customTitle.trim().length == 0) {
+      customTitle = '{{page}} - {{sitename}}';
+    }
+
+    return customTitle
+      .replace('{{sitename}}', this.appTitle(config))
+      .replace('{{page}}', page);
+  }
+
   configSchema.statics.behaviorType = function(config)
   configSchema.statics.behaviorType = function(config)
   {
   {
     const key = 'customize:behavior';
     const key = 'customize:behavior';
@@ -465,7 +491,7 @@ module.exports = function(crowi) {
 
 
     const local_config = {
     const local_config = {
       crowi: {
       crowi: {
-        title: config.crowi['app:title'],
+        title: Config.appTitle(crowi),
         url: config.crowi['app:url'] || '',
         url: config.crowi['app:url'] || '',
       },
       },
       upload: {
       upload: {

+ 3 - 0
lib/models/index.js

@@ -2,8 +2,11 @@
 
 
 module.exports = {
 module.exports = {
   Page: require('./page'),
   Page: require('./page'),
+  PageGroupRelation: require('./page-group-relation'),
   User: require('./user'),
   User: require('./user'),
   ExternalAccount: require('./external-account'),
   ExternalAccount: require('./external-account'),
+  UserGroup: require('./user-group'),
+  UserGroupRelation: require('./user-group-relation'),
   Revision: require('./revision'),
   Revision: require('./revision'),
   Bookmark: require('./bookmark'),
   Bookmark: require('./bookmark'),
   Comment: require('./comment'),
   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 - 12
lib/models/page.js

@@ -7,6 +7,7 @@ module.exports = function(crowi) {
     , GRANT_RESTRICTED = 2
     , GRANT_RESTRICTED = 2
     , GRANT_SPECIFIED = 3
     , GRANT_SPECIFIED = 3
     , GRANT_OWNER = 4
     , GRANT_OWNER = 4
+    , GRANT_USER_GROUP = 5
     , PAGE_GRANT_ERROR = 1
     , PAGE_GRANT_ERROR = 1
 
 
     , STATUS_WIP        = 'wip'
     , STATUS_WIP        = 'wip'
@@ -340,6 +341,7 @@ module.exports = function(crowi) {
     grantLabels[GRANT_PUBLIC]     = 'Public'; // 公開
     grantLabels[GRANT_PUBLIC]     = 'Public'; // 公開
     grantLabels[GRANT_RESTRICTED] = 'Anyone with the link'; // リンクを知っている人のみ
     grantLabels[GRANT_RESTRICTED] = 'Anyone with the link'; // リンクを知っている人のみ
     //grantLabels[GRANT_SPECIFIED]  = 'Specified users only'; // 特定ユーザーのみ
     //grantLabels[GRANT_SPECIFIED]  = 'Specified users only'; // 特定ユーザーのみ
+    grantLabels[GRANT_USER_GROUP] = 'Only inside the group'; // 特定グループのみ
     grantLabels[GRANT_OWNER]      = 'Just me'; // 自分のみ
     grantLabels[GRANT_OWNER]      = 'Just me'; // 自分のみ
 
 
     return grantLabels;
     return grantLabels;
@@ -391,7 +393,6 @@ module.exports = function(crowi) {
       /^\/_.*/, // /_api/* and so on
       /^\/_.*/, // /_api/* and so on
       /^\/\-\/.*/,
       /^\/\-\/.*/,
       /^\/_r\/.*/,
       /^\/_r\/.*/,
-      /^\/user\/[^\/]+\/(bookmarks|comments|activities|pages|recent-create|recent-edit)/, // reserved
       /^\/?https?:\/\/.+$/, // avoid miss in renaming
       /^\/?https?:\/\/.+$/, // avoid miss in renaming
       /\/{2,}/,             // avoid miss in renaming
       /\/{2,}/,             // avoid miss in renaming
       /\s+\/\s+/,           // avoid miss in renaming
       /\s+\/\s+/,           // avoid miss in renaming
@@ -454,15 +455,26 @@ module.exports = function(crowi) {
 
 
   pageSchema.statics.findPageByIdAndGrantedUser = function(id, userData) {
   pageSchema.statics.findPageByIdAndGrantedUser = function(id, userData) {
     var Page = this;
     var Page = this;
+    var PageGroupRelation = crowi.model('PageGroupRelation');
+    var pageData = null;
 
 
     return new Promise(function(resolve, reject) {
     return new Promise(function(resolve, reject) {
       Page.findPageById(id)
       Page.findPageById(id)
-      .then(function(pageData) {
+      .then(function(result) {
+        pageData = result;
         if (userData && !pageData.isGrantedFor(userData)) {
         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 reject(new Error('Page is not granted for the user')); //PAGE_GRANT_ERROR, null);
         }
         }
-
-        return resolve(pageData);
       }).catch(function(err) {
       }).catch(function(err) {
         return reject(err);
         return reject(err);
       });
       });
@@ -472,6 +484,7 @@ module.exports = function(crowi) {
   // find page and check if granted user
   // find page and check if granted user
   pageSchema.statics.findPage = function(path, userData, revisionId, ignoreNotFound) {
   pageSchema.statics.findPage = function(path, userData, revisionId, ignoreNotFound) {
     var self = this;
     var self = this;
+    var PageGroupRelation = crowi.model('PageGroupRelation');
 
 
     return new Promise(function(resolve, reject) {
     return new Promise(function(resolve, reject) {
       self.findOne({path: path}, function(err, pageData) {
       self.findOne({path: path}, function(err, pageData) {
@@ -490,10 +503,22 @@ module.exports = function(crowi) {
         }
         }
 
 
         if (!pageData.isGrantedFor(userData)) {
         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 +783,15 @@ module.exports = function(crowi) {
     });
     });
   };
   };
 
 
-  pageSchema.statics.updateGrant = function(page, grant, userData) {
+  pageSchema.statics.updateGrant = function (page, grant, userData, grantUserGroupId) {
     var Page = this;
     var Page = this;
 
 
+    if (grant == GRANT_USER_GROUP && grantUserGroupId == null) {
+      throw new Error('grant userGroupId is not specified');
+    }
     return new Promise(function(resolve, reject) {
     return new Promise(function(resolve, reject) {
       page.grant = grant;
       page.grant = grant;
-      if (grant == GRANT_PUBLIC) {
+      if (grant == GRANT_PUBLIC || grant == GRANT_USER_GROUP) {
         page.grantedUsers = [];
         page.grantedUsers = [];
       } else {
       } else {
         page.grantedUsers = [];
         page.grantedUsers = [];
@@ -776,11 +804,38 @@ module.exports = function(crowi) {
           return reject(err);
           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 でいいのでは
   // Instance method でいいのでは
   pageSchema.statics.pushToGrantedUsers = function(page, userData) {
   pageSchema.statics.pushToGrantedUsers = function(page, userData) {
 
 
@@ -837,7 +892,8 @@ module.exports = function(crowi) {
       , Revision = crowi.model('Revision')
       , Revision = crowi.model('Revision')
       , format = options.format || 'markdown'
       , format = options.format || 'markdown'
       , grant = options.grant || GRANT_PUBLIC
       , grant = options.grant || GRANT_PUBLIC
-      , redirectTo = options.redirectTo || null;
+      , redirectTo = options.redirectTo || null
+      , grantUserGroupId = options.grantUserGroupId || null;
 
 
     // force public
     // force public
       if (isPortalPath(path)) {
       if (isPortalPath(path)) {
@@ -867,6 +923,12 @@ module.exports = function(crowi) {
               return reject(err);
               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});
             var newRevision = Revision.prepareRevision(newPage, body, user, {format: format});
             Page.pushRevision(newPage, newRevision, user).then(function(data) {
             Page.pushRevision(newPage, newRevision, user).then(function(data) {
               resolve(data);
               resolve(data);
@@ -884,6 +946,7 @@ module.exports = function(crowi) {
     var Page = this
     var Page = this
       , Revision = crowi.model('Revision')
       , Revision = crowi.model('Revision')
       , grant = options.grant || null
       , grant = options.grant || null
+      , grantUserGroupId = options.grantUserGroupId || null
       ;
       ;
     // update existing page
     // update existing page
     var newRevision = Revision.prepareRevision(pageData, body, user);
     var newRevision = Revision.prepareRevision(pageData, body, user);
@@ -892,7 +955,7 @@ module.exports = function(crowi) {
       Page.pushRevision(pageData, newRevision, user)
       Page.pushRevision(pageData, newRevision, user)
       .then(function(revision) {
       .then(function(revision) {
         if (grant != pageData.grant) {
         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);
             debug('Page grant update:', data);
             resolve(data);
             resolve(data);
             pageEvent.emit('update', data, user);
             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
   // // create a XSS Filter instance
   // // TODO read options
   // // 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);
+}
+

+ 12 - 2
lib/models/user.js

@@ -29,6 +29,7 @@ module.exports = function(crowi) {
     userId: String,
     userId: String,
     image: String,
     image: String,
     isGravatarEnabled: { type: Boolean, default: false },
     isGravatarEnabled: { type: Boolean, default: false },
+    isEmailPublished: { type: Boolean, default: true },
     googleId: String,
     googleId: String,
     name: { type: String },
     name: { type: String },
     username: { type: String, required: true, unique: true },
     username: { type: String, required: true, unique: true },
@@ -155,6 +156,14 @@ module.exports = function(crowi) {
     });
     });
   };
   };
 
 
+
+  userSchema.methods.updateIsEmailPublished = function(isEmailPublished, callback) {
+    this.isEmailPublished = isEmailPublished;
+    this.save(function(err, userData) {
+      return callback(err, userData);
+    });
+  };
+
   userSchema.methods.updatePassword = function(password, callback) {
   userSchema.methods.updatePassword = function(password, callback) {
     this.setPassword(password);
     this.setPassword(password);
     this.save(function(err, userData) {
     this.save(function(err, userData) {
@@ -565,6 +574,7 @@ module.exports = function(crowi) {
   userSchema.statics.createUsersByInvitation = function(emailList, toSendEmail, callback) {
   userSchema.statics.createUsersByInvitation = function(emailList, toSendEmail, callback) {
     var User = this
     var User = this
       , createdUserList = []
       , createdUserList = []
+      , Config = crowi.model('Config')
       , config = crowi.getConfig()
       , config = crowi.getConfig()
       , mailer = crowi.getMailer()
       , mailer = crowi.getMailer()
       ;
       ;
@@ -641,13 +651,13 @@ module.exports = function(crowi) {
 
 
               mailer.send({
               mailer.send({
                   to: user.email,
                   to: user.email,
-                  subject: 'Invitation to ' + config.crowi['app:title'],
+                  subject: 'Invitation to ' + Config.appTitle(config),
                   template: 'admin/userInvitation.txt',
                   template: 'admin/userInvitation.txt',
                   vars: {
                   vars: {
                     email: user.email,
                     email: user.email,
                     password: user.password,
                     password: user.password,
                     url: config.crowi['app:url'],
                     url: config.crowi['app:url'],
-                    appTitle: config.crowi['app:title'],
+                    appTitle: Config.appTitle(config),
                   }
                   }
                 },
                 },
                 function (err, s) {
                 function (err, s) {

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

@@ -11,10 +11,10 @@ class PluginUtils {
    * return a definition objects that has following structure:
    * 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: [
    *   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
    * borrowing from: https://github.com/hexojs/hexo/blob/d1db459c92a4765620343b95789361cbbc6414c5/lib/hexo/load_plugins.js#L17
    *
    *
    * @returns array of objects
    * @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 = {};
     let objs = {};
     Object.keys(deps).forEach((name) => {
     Object.keys(deps).forEach((name) => {
-      if (/^crowi-plugin-/.test(name)) {
+      if (/^(crowi|growi)-plugin-/.test(name)) {
         objs[name] = deps[name];
         objs[name] = deps[name];
       }
       }
     });
     });

+ 315 - 7
lib/routes/admin.js

@@ -2,16 +2,20 @@ module.exports = function(crowi, app) {
   'use strict';
   'use strict';
 
 
   var debug = require('debug')('crowi:routes:admin')
   var debug = require('debug')('crowi:routes:admin')
+    , fs = require('fs')
     , models = crowi.models
     , models = crowi.models
     , Page = models.Page
     , Page = models.Page
+    , PageGroupRelation = models.PageGroupRelation
     , User = models.User
     , User = models.User
     , ExternalAccount = models.ExternalAccount
     , ExternalAccount = models.ExternalAccount
+    , UserGroup = models.UserGroup
+    , UserGroupRelation = models.UserGroupRelation
     , Config = models.Config
     , Config = models.Config
     , PluginUtils = require('../plugins/plugin-utils')
     , PluginUtils = require('../plugins/plugin-utils')
     , pluginUtils = new PluginUtils()
     , pluginUtils = new PluginUtils()
     , ApiResponse = require('../util/apiResponse')
     , ApiResponse = require('../util/apiResponse')
 
 
-    , MAX_PAGE_LIST = 5
+    , MAX_PAGE_LIST = 50
     , actions = {};
     , actions = {};
 
 
   function createPager(total, limit, page, pagesCount, maxPageList) {
   function createPager(total, limit, page, pagesCount, maxPageList) {
@@ -244,11 +248,6 @@ module.exports = function(crowi, app) {
 
 
   actions.search = {};
   actions.search = {};
   actions.search.index = function(req, res) {
   actions.search.index = function(req, res) {
-    var search = crowi.getSearcher();
-    if (!search) {
-      return res.redirect('/admin');
-    }
-
     return res.render('admin/search', {
     return res.render('admin/search', {
     });
     });
   };
   };
@@ -395,7 +394,7 @@ module.exports = function(crowi, app) {
     User.findById(id, function(err, userData) {
     User.findById(id, function(err, userData) {
       userData.statusActivate(function(err, userData) {
       userData.statusActivate(function(err, userData) {
         if (err === null) {
         if (err === null) {
-          req.flash('successMessage', userData.name + 'さんのアカウントを承認しました');
+          req.flash('successMessage', userData.name + 'さんのアカウントを有効化しました');
         } else {
         } else {
           req.flash('errorMessage', '更新に失敗しました。');
           req.flash('errorMessage', '更新に失敗しました。');
           debug(err, userData);
           debug(err, userData);
@@ -527,6 +526,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 = {};
   actions.api.appSetting = function(req, res) {
   actions.api.appSetting = function(req, res) {
     var form = req.form.settingForm;
     var form = req.form.settingForm;

+ 15 - 3
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/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/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/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/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/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);
   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.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);
   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'                       , loginRequired(crowi, app) , me.index);
   app.get('/me/password'              , loginRequired(crowi, app) , me.password);
   app.get('/me/password'              , loginRequired(crowi, app) , me.password);
   app.get('/me/apiToken'              , loginRequired(crowi, app) , me.apiToken);
   app.get('/me/apiToken'              , loginRequired(crowi, app) , me.apiToken);
@@ -135,9 +150,6 @@ module.exports = function(crowi, app) {
   app.post('/_api/me/picture/upload'  , loginRequired(crowi, app) , uploads.single('userPicture'), me.api.uploadPicture);
   app.post('/_api/me/picture/upload'  , loginRequired(crowi, app) , uploads.single('userPicture'), me.api.uploadPicture);
   app.get( '/_api/user/bookmarks'     , loginRequired(crowi, app, false) , user.api.bookmarks);
   app.get( '/_api/user/bookmarks'     , loginRequired(crowi, app, false) , user.api.bookmarks);
 
 
-  app.get( '/user/:username([^/]+)/bookmarks'      , loginRequired(crowi, app, false) , page.userBookmarkList);
-  app.get( '/user/:username([^/]+)/recent-create'  , loginRequired(crowi, app, false) , page.userRecentCreatedList);
-
   // HTTP RPC Styled API (に徐々に移行していいこうと思う)
   // HTTP RPC Styled API (に徐々に移行していいこうと思う)
   app.get('/_api/users.list'          , accessTokenParser , loginRequired(crowi, app, false) , user.api.list);
   app.get('/_api/users.list'          , accessTokenParser , loginRequired(crowi, app, false) , user.api.list);
   app.get('/_api/pages.list'          , accessTokenParser , loginRequired(crowi, app, false) , page.api.list);
   app.get('/_api/pages.list'          , accessTokenParser , loginRequired(crowi, app, false) , page.api.list);

+ 25 - 7
lib/routes/installer.js

@@ -2,6 +2,8 @@ module.exports = function(crowi, app) {
   'use strict';
   'use strict';
 
 
   var debug = require('debug')('crowi:routes:installer')
   var debug = require('debug')('crowi:routes:installer')
+    , path = require('path')
+    , fs = require('graceful-fs')
     , models = crowi.models
     , models = crowi.models
     , Config = models.Config
     , Config = models.Config
     , User = models.User
     , User = models.User
@@ -9,13 +11,25 @@ module.exports = function(crowi, app) {
 
 
     , actions = {};
     , actions = {};
 
 
+  function createInitialPages(owner, lang) {
+    // create portal page for '/'
+    const welcomeMarkdownPath = path.join(crowi.localeDir, lang, 'welcome.md');
+    const welcomeMarkdown = fs.readFileSync(welcomeMarkdownPath);
+    Page.create('/', welcomeMarkdown, owner, {});
+
+    // create /Sandbox
+    const sandboxMarkdownPath = path.join(crowi.localeDir, lang, 'sandbox.md');
+    const sandboxMarkdown = fs.readFileSync(sandboxMarkdownPath);
+    Page.create('/Sandbox', sandboxMarkdown, owner, {});
+  }
+
   actions.index = function(req, res) {
   actions.index = function(req, res) {
     return res.render('installer');
     return res.render('installer');
   };
   };
 
 
   actions.createAdmin = function(req, res) {
   actions.createAdmin = function(req, res) {
     var registerForm = req.body.registerForm || {};
     var registerForm = req.body.registerForm || {};
-    var language = req.language || 'en';
+    var language = req.language || 'en-US';
 
 
     if (req.form.isValid) {
     if (req.form.isValid) {
       var name = registerForm.name;
       var name = registerForm.name;
@@ -37,14 +51,18 @@ module.exports = function(crowi, app) {
               return ;
               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, {});
+          // create initial pages
+          createInitialPages(userData, language);
         });
         });
       });
       });
     } else {
     } else {

+ 3 - 2
lib/routes/login.js

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

+ 3 - 2
lib/routes/me.js

@@ -83,7 +83,8 @@ module.exports = function(crowi, app) {
     if (req.method == 'POST' && req.form.isValid) {
     if (req.method == 'POST' && req.form.isValid) {
       var name = userForm.name;
       var name = userForm.name;
       var email = userForm.email;
       var email = userForm.email;
-      var lang= userForm.lang;
+      var lang = userForm.lang;
+      var isEmailPublished = userForm.isEmailPublished;
 
 
       /*
       /*
        * disabled because the system no longer allows undefined email -- 2017.10.06 Yuki Takei
        * disabled because the system no longer allows undefined email -- 2017.10.06 Yuki Takei
@@ -96,7 +97,7 @@ module.exports = function(crowi, app) {
 
 
       User.findOneAndUpdate(
       User.findOneAndUpdate(
         { email: userData.email },                  // query
         { email: userData.email },                  // query
-        { name, email, lang },                      // updating data
+        { name, email, lang, isEmailPublished },                      // updating data
         { runValidators: true, context: 'query' },  // for validation
         { runValidators: true, context: 'query' },  // for validation
                                                     //   see https://www.npmjs.com/package/mongoose-unique-validator#find--updates -- 2017.09.24 Yuki Takei
                                                     //   see https://www.npmjs.com/package/mongoose-unique-validator#find--updates -- 2017.09.24 Yuki Takei
         (err) => {
         (err) => {

+ 44 - 98
lib/routes/page.js

@@ -8,6 +8,7 @@ module.exports = function(crowi, app) {
     , config   = crowi.getConfig()
     , config   = crowi.getConfig()
     , Revision = crowi.model('Revision')
     , Revision = crowi.model('Revision')
     , Bookmark = crowi.model('Bookmark')
     , Bookmark = crowi.model('Bookmark')
+    , UserGroupRelation = crowi.model('UserGroupRelation')
     , ApiResponse = require('../util/apiResponse')
     , ApiResponse = require('../util/apiResponse')
     , interceptorManager = crowi.getInterceptorManager()
     , interceptorManager = crowi.getInterceptorManager()
 
 
@@ -69,11 +70,11 @@ module.exports = function(crowi, app) {
   actions.pageListShowWrapper = function(req, res) {
   actions.pageListShowWrapper = function(req, res) {
     const behaviorType = Config.behaviorType(config);
     const behaviorType = Config.behaviorType(config);
 
 
-    if ('crowi-plus' === behaviorType) {
-      return actions.pageListShowForCrowiPlus(req, res);
+    if (!behaviorType || 'crowi' === behaviorType) {
+      return actions.pageListShow(req, res);
     }
     }
     else {
     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) {
   actions.pageShowWrapper = function(req, res) {
     const behaviorType = Config.behaviorType(config);
     const behaviorType = Config.behaviorType(config);
 
 
-    if ('crowi-plus' === behaviorType) {
-      return actions.pageShowForCrowiPlus(req, res);
+    if (!behaviorType || 'crowi' === behaviorType) {
+      return actions.pageShow(req, res);
     }
     }
     else {
     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) {
   actions.trashPageListShowWrapper = function(req, res) {
     const behaviorType = Config.behaviorType(config);
     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 {
     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) {
   actions.trashPageShowWrapper = function(req, res) {
     const behaviorType = Config.behaviorType(config);
     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/'
       // redirect to '/trash/'
       return res.redirect('/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) {
   actions.deletedPageListShowWrapper = function(req, res) {
     const behaviorType = Config.behaviorType(config);
     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 {
     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 = {
     var renderVars = {
       page: null,
       page: null,
       path: path,
       path: path,
+      isPortal: false,
       pages: [],
       pages: [],
       tree: [],
       tree: [],
     };
     };
@@ -167,6 +169,7 @@ module.exports = function(crowi, app) {
     Page.hasPortalPage(path, req.user, req.query.revision)
     Page.hasPortalPage(path, req.user, req.query.revision)
     .then(function(portalPage) {
     .then(function(portalPage) {
       renderVars.page = portalPage;
       renderVars.page = portalPage;
+      renderVars.isPortal = (portalPage != null);
 
 
       if (portalPage) {
       if (portalPage) {
         renderVars.revision = portalPage.revision;
         renderVars.revision = portalPage.revision;
@@ -231,6 +234,7 @@ module.exports = function(crowi, app) {
       author: false,
       author: false,
       pages: [],
       pages: [],
       tree: [],
       tree: [],
+      userRelatedGroups: [],
     };
     };
 
 
     var pageTeamplate = 'customlayout-selector/page';
     var pageTeamplate = 'customlayout-selector/page';
@@ -331,6 +335,15 @@ module.exports = function(crowi, app) {
           debug('Error on rendering pageListShowForCrowiPlus', err);
           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) {
   function renderPage(pageData, req, res) {
     // create page
     // create page
     if (!pageData) {
     if (!pageData) {
-      return res.render('page', {
+      return res.render('customlayout-selector/not_found', {
         author: {},
         author: {},
         page: false,
         page: false,
       });
       });
@@ -548,6 +561,7 @@ module.exports = function(crowi, app) {
     var currentRevision = pageForm.currentRevision;
     var currentRevision = pageForm.currentRevision;
     var grant = pageForm.grant;
     var grant = pageForm.grant;
     var path = pageForm.path;
     var path = pageForm.path;
+    var grantUserGroupId = pageForm.grantUserGroupId
 
 
     // TODO: make it pluggable
     // TODO: make it pluggable
     var notify = pageForm.notify || {};
     var notify = pageForm.notify || {};
@@ -586,11 +600,11 @@ module.exports = function(crowi, app) {
 
 
       if (data) {
       if (data) {
         previousRevision = data.revision;
         previousRevision = data.revision;
-        return Page.updatePage(data, body, req.user, {grant: grant});
+        return Page.updatePage(data, body, req.user, { grant: grant, grantUserGroupId: grantUserGroupId});
       } else {
       } else {
         // new page
         // new page
         updateOrCreate = 'create';
         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) {
     }).then(function(data) {
       // data is a saved page data.
       // data is a saved page data.
@@ -623,80 +637,7 @@ module.exports = function(crowi, app) {
     });
     });
   };
   };
 
 
-  // app.get( '/users/:username([^/]+)/bookmarks'      , loginRequired(crowi, app) , page.userBookmarkList);
-  actions.userBookmarkList = function(req, res) {
-    var username = req.params.username;
-    var limit = 50;
-    var offset = parseInt(req.query.offset)  || 0;
-
-    var user;
-    var renderVars = {};
 
 
-    var pagerOptions = { offset: offset, limit : limit };
-    var queryOptions = { offset: offset, limit : limit + 1, populatePage: true, requestUser: req.user};
-
-    User.findUserByUsername(username)
-    .then(function(user) {
-      if (user === null) {
-        throw new Error('The user not found.');
-      }
-      renderVars.pageUser = user;
-
-      return Bookmark.findByUser(user, queryOptions);
-    }).then(function(bookmarks) {
-
-      if (bookmarks.length > limit) {
-        bookmarks.pop();
-      }
-      pagerOptions.length = bookmarks.length;
-
-      renderVars.pager = generatePager(pagerOptions);
-      renderVars.bookmarks = bookmarks;
-
-      return res.render('user/bookmarks', renderVars);
-    }).catch(function(err) {
-      debug('Error on rendereing bookmark', err);
-      res.redirect('/');
-    });
-  };
-
-  // app.get( '/users/:username([^/]+)/recent-create' , loginRequired(crowi, app) , page.userRecentCreatedList);
-  actions.userRecentCreatedList = function(req, res) {
-    var username = req.params.username;
-    var limit = 50;
-    var offset = parseInt(req.query.offset) || 0;
-
-    var user;
-    var renderVars = {};
-
-    var pagerOptions = { offset: offset, limit : limit };
-    var queryOptions = { offset: offset, limit : limit + 1};
-
-
-    User.findUserByUsername(username)
-    .then(function(user) {
-      if (user === null) {
-        throw new Error('The user not found.');
-      }
-      renderVars.pageUser = user;
-
-      return Page.findListByCreator(user, queryOptions, req.user);
-    }).then(function(pages) {
-
-      if (pages.length > limit) {
-        pages.pop();
-      }
-      pagerOptions.length = pages.length;
-
-      renderVars.pager = generatePager(pagerOptions);
-      renderVars.pages = pages;
-
-      return res.render('user/recent-create', renderVars);
-    }).catch(function(err) {
-      debug('Error on rendereing recent-created', err);
-      res.redirect('/');
-    });
-  };
 
 
   var api = actions.api = {};
   var api = actions.api = {};
 
 
@@ -788,6 +729,7 @@ module.exports = function(crowi, app) {
     var body = req.body.body || null;
     var body = req.body.body || null;
     var pagePath = req.body.path || null;
     var pagePath = req.body.path || null;
     var grant = req.body.grant || null;
     var grant = req.body.grant || null;
+    var grantUserGroupId = req.body.grantUserGroupId || null;
 
 
     if (body === null || pagePath === null) {
     if (body === null || pagePath === null) {
       return res.json(ApiResponse.error('Parameters body and path are required.'));
       return res.json(ApiResponse.error('Parameters body and path are required.'));
@@ -800,7 +742,7 @@ module.exports = function(crowi, app) {
         throw new Error('Page exists');
         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) {
     }).then(function(data) {
       if (!data) {
       if (!data) {
         throw new Error('Failed to create page.');
         throw new Error('Failed to create page.');
@@ -835,6 +777,7 @@ module.exports = function(crowi, app) {
     var pageId = req.body.page_id || null;
     var pageId = req.body.page_id || null;
     var revisionId = req.body.revision_id || null;
     var revisionId = req.body.revision_id || null;
     var grant = req.body.grant || null;
     var grant = req.body.grant || null;
+    var grantUserGroupId = req.body.grantUserGroupId || null;
 
 
     if (pageId === null || pageBody === null) {
     if (pageId === null || pageBody === null) {
       return res.json(ApiResponse.error('page_id and body are required.'));
       return res.json(ApiResponse.error('page_id and body are required.'));
@@ -850,6 +793,9 @@ module.exports = function(crowi, app) {
       if (grant !== null) {
       if (grant !== null) {
         grantOption.grant = grant;
         grantOption.grant = grant;
       }
       }
+      if (grantUserGroupId != null) {
+        grantOption.grantUserGroupId = grantUserGroupId;
+      }
       return Page.updatePage(pageData, pageBody, req.user, grantOption);
       return Page.updatePage(pageData, pageBody, req.user, grantOption);
     }).then(function(pageData) {
     }).then(function(pageData) {
       var result = {
       var result = {

+ 2 - 1
lib/util/mailer.js

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

+ 2 - 2
lib/util/middlewares.js

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

+ 1 - 1
lib/util/search.js

@@ -244,7 +244,7 @@ SearchClient.prototype.addAllPages = function()
       // all done
       // all done
 
 
       // return if body is empty
       // 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) {
       if (body.length == 0) {
         return resolve();
         return resolve();
       }
       }

+ 1 - 1
lib/util/slack.js

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

+ 39 - 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 : '-';
     return crowi.runtimeVersions.versions.yarn ? crowi.runtimeVersions.versions.yarn.version : '-';
   }
   }
 
 
-  locals.crowiVersion = function() {
+  locals.growiVersion = function() {
     return crowi.version;
     return crowi.version;
   }
   }
 
 
@@ -25,11 +25,31 @@ module.exports = function(crowi, app, req, locals) {
     return req.csrfToken;
     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
    * return true if enabled
    */
    */
   locals.isEnabledPassport = function() {
   locals.isEnabledPassport = function() {
-    var config = crowi.getConfig()
+    var config = crowi.getConfig();
     return Config.isEnabledPassport(config);
     return Config.isEnabledPassport(config);
   }
   }
 
 
@@ -58,6 +78,12 @@ module.exports = function(crowi, app, req, locals) {
   }
   }
 
 
   locals.googleLoginEnabled = function() {
   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()
     var config = crowi.getConfig()
     return config.crowi['google:clientId'] && config.crowi['google:clientSecret'];
     return config.crowi['google:clientId'] && config.crowi['google:clientSecret'];
   };
   };
@@ -97,8 +123,18 @@ module.exports = function(crowi, app, req, locals) {
     return Config.customHeader(config);
     return Config.customHeader(config);
   }
   }
 
 
-  locals.behaviorType = function() {
+  locals.theme = function() {
     var config = crowi.getConfig()
     var config = crowi.getConfig()
+    return Config.theme(config);
+  }
+
+  locals.customTitle = function(page) {
+    const config = crowi.getConfig();
+    return Config.customTitle(config, page);
+  }
+
+  locals.behaviorType = function() {
+    var config = crowi.getConfig();
     return Config.behaviorType(config);
     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" 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[path]" value="{{ path }}">
   <input type="hidden" name="pageForm[currentRevision]" value="{{ pageForm.currentRevision|default(page.revision._id.toString()) }}">
   <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 id="page-editor-options-selector"></div>
     </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() %}
       {% 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"
         <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"
           id="page-form-slack-channel"
           data-toggle="popover"
           data-toggle="popover"
@@ -54,14 +49,21 @@
       {% if forceGrant %}
       {% if forceGrant %}
       <input type="hidden" name="pageForm[grant]" value="{{ forceGrant }}">
       <input type="hidden" name="pageForm[grant]" value="{{ forceGrant }}">
       {% else %}
       {% 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 %}
         {% 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 %}
         {% endfor %}
       </select>
       </select>
       {% endif %}
       {% endif %}
       <input type="hidden" id="edit-form-csrf" name="_csrf" value="{{ csrf() }}">
       <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>
   </div>
   </div>
 </form>
 </form>

+ 25 - 21
lib/views/admin/app.html

@@ -1,8 +1,8 @@
 {% extends '../layout/admin.html' %}
 {% extends '../layout/admin.html' %}
 
 
-{% block html_title %}{{ t('App settings') }} · {% endblock %}
+{% block html_title %}{{ customTitle(t('App settings')) }}{% endblock %}
 
 
-{% block content_head %}
+{% block content_header %}
 <div class="header-wrap">
 <div class="header-wrap">
   <header id="page-header">
   <header id="page-header">
     <h1 class="title" id="">{{ t('App settings') }}</h1>
     <h1 class="title" id="">{{ t('App settings') }}</h1>
@@ -36,11 +36,10 @@
       <fieldset>
       <fieldset>
         <legend>{{ t('App settings') }}</legend>
         <legend>{{ t('App settings') }}</legend>
         <div class="form-group">
         <div class="form-group">
-          <label for="settingForm[app:title]" class="col-xs-3 control-label">{{ t('app_setting.Wiki name') }}</label>
+          <label for="settingForm[app:title]" class="col-xs-3 control-label">{{ t('app_setting.Site Name') }}</label>
           <div class="col-xs-6">
           <div class="col-xs-6">
-            <input class="form-control" type="text" name="settingForm[app:title]" value="{{ settingForm['app:title'] }}">
-
-            <p class="help-block">{{ t("app_setting.wiki_change") }}</p>
+            <input class="form-control" type="text" name="settingForm[app:title]" value="{{ settingForm['app:title'] | default('') }}">
+            <p class="help-block">{{ t("app_setting.sitename_change") }}</p>
           </div>
           </div>
         </div>
         </div>
 
 
@@ -54,16 +53,20 @@
 
 
         <div class="form-group">
         <div class="form-group">
           <div class="col-xs-offset-3 col-xs-6">
           <div class="col-xs-offset-3 col-xs-6">
-            <input type="checkbox" name="settingForm[app:fileUpload]" value="1"
-              {% if settingForm['app:fileUpload'] %}
-              checked
-              {% endif %}
-              {% if not isUploadable() %}
-              disabled="disabled"
-              {% else %}
-              {% endif %}
-              >
-              <label for="settingForm[app:fileUpload]" class="">{{ t("app_setting.enable_files_except_image") }}</label>
+            <div class="checkbox checkbox-info">
+              <input type="checkbox" id="cbFileUpload" name="settingForm[app:fileUpload]" value="1"
+                {% if settingForm['app:fileUpload'] %}
+                checked
+                {% endif %}
+                {% if not isUploadable() %}
+                disabled="disabled"
+                {% else %}
+                {% endif %}
+                >
+              <label for="cbFileUpload">
+                {{ t("app_setting.enable_files_except_image") }}
+              </label>
+            </div>
 
 
               <p class="help-block">
               <p class="help-block">
                 {{ t("app_setting.enable_files_except_image") }}<br>
                 {{ t("app_setting.enable_files_except_image") }}<br>
@@ -133,7 +136,7 @@
         {{ t("app_setting.No_SMTP_setting") }}<br>
         {{ t("app_setting.No_SMTP_setting") }}<br>
           <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>
         </p>
 
 
         <div class="form-group">
         <div class="form-group">
@@ -183,14 +186,15 @@
         <div class="form-group">
         <div class="form-group">
           <label for="settingForm[plugin:isEnabledPlugins]" class="col-xs-3 control-label">{{ t('app_setting.Load plugins') }}</label>
           <label for="settingForm[plugin:isEnabledPlugins]" class="col-xs-3 control-label">{{ t('app_setting.Load plugins') }}</label>
           <div class="col-xs-6">
           <div class="col-xs-6">
+
             <div class="btn-group btn-toggle" data-toggle="buttons">
             <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"
                 <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>
-              <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"
                 <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>
               </label>
             </div>
             </div>
           </div>
           </div>

+ 163 - 51
lib/views/admin/customize.html

@@ -1,6 +1,11 @@
 {% extends '../layout/admin.html' %}
 {% extends '../layout/admin.html' %}
 
 
-{% block html_title %}{{ t('Customize') }} {% endblock %}
+{% block html_title %}{{ customTitle(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 %}
 {% block html_additional_headers %}
   {% parent %}
   {% parent %}
@@ -13,7 +18,7 @@
   </style>
   </style>
 {% endblock %}
 {% endblock %}
 
 
-{% block content_head %}
+{% block content_header %}
 <div class="header-wrap">
 <div class="header-wrap">
   <header id="page-header">
   <header id="page-header">
     <h1 class="title" id="">{{ t('Customize') }} </h1>
     <h1 class="title" id="">{{ t('Customize') }} </h1>
@@ -43,37 +48,76 @@
     </div>
     </div>
     <div class="col-md-9">
     <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">
       <form action="/_api/admin/customize/behavior" method="post" class="form-horizontal" id="cutombehaviorSettingForm" role="form">
       <fieldset>
       <fieldset>
         <legend>{{ t('customize_page.Behavior') }}</legend>
         <legend>{{ t('customize_page.Behavior') }}</legend>
 
 
+        {% set isBehaviorGrowi = 'growi' === settingForm['customize:behavior'] || 'crowi-plus' === settingForm['customize:behavior'] %}
         <div class="form-group">
         <div class="form-group">
           <div class="col-xs-6">
           <div class="col-xs-6">
             <h4>
             <h4>
-              <input type="radio" name="settingForm[customize:behavior]" value="crowi"
-                  {% if !settingForm['customize:behavior'] || 'crowi' === settingForm['customize:behavior'] %}checked="checked"{% endif %}>
-              Official Crowi Behavior
+              <div class="radio radio-primary">
+                <input type="radio" id="radioBehaviorGrowi" name="settingForm[customize:behavior]" value="growi"
+                    {% if isBehaviorGrowi %}checked="checked"{% endif %}>
+                <label for="radioBehaviorGrowi">
+                  GROWI Simplified Behavior <small class="text-success">(Recommended)</small>
+                </label>
+            </div>
             </h4>
             </h4>
             <ul>
             <ul>
-              <li><code>/page</code> shows the page</li>
-              <li><code>/page/</code> shows the list of sub pages</li>
-              <ul>
-                <li>If portal is applied to <code>/page/</code> , the portal and the list of sub pages are shown</li>
-              </ul>
+              <li>Both of <code>/page</code> and <code>/page/</code> shows the same page</li>
               <li><code>/nonexistent_page</code> shows editing form</li>
               <li><code>/nonexistent_page</code> shows editing form</li>
-              <li><code>/nonexistent_page/</code> the list of sub pages</li>
+              <li>All pages shows the list of sub pages <b>if using GROWI Enhanced Layout</b></li>
             </ul>
             </ul>
           </div>
           </div>
           <div class="col-xs-6">
           <div class="col-xs-6">
             <h4>
             <h4>
-              <input type="radio" name="settingForm[customize:behavior]" value="crowi-plus"
-                  {% if 'crowi-plus' === settingForm['customize:behavior'] %}checked="checked"{% endif %}>
-              crowi-plus Simplified Behavior <small class="text-success">(Recommended)</small>
+              <div class="radio radio-primary">
+                <input type="radio" id="radioBehaviorCrowi" name="settingForm[customize:behavior]" value="crowi"
+                    {% if !isBehaviorGrowi %}checked="checked"{% endif %}>
+                <label for="radioBehaviorCrowi">
+                  Crowi Classic Behavior
+                </label>
+              </div>
             </h4>
             </h4>
             <ul>
             <ul>
-              <li>Both of <code>/page</code> and <code>/page/</code> shows the same page</li>
+              <li><code>/page</code> shows the page</li>
+              <li><code>/page/</code> shows the list of sub pages</li>
+              <ul>
+                <li>If portal is applied to <code>/page/</code> , the portal and the list of sub pages are shown</li>
+              </ul>
               <li><code>/nonexistent_page</code> shows editing form</li>
               <li><code>/nonexistent_page</code> shows editing form</li>
-              <li>All pages shows the list of sub pages when using <b>crowi-plus Enhanced Layout</b></li>
+              <li><code>/nonexistent_page/</code> the list of sub pages</li>
             </ul>
             </ul>
           </div>
           </div>
         </div>
         </div>
@@ -93,39 +137,48 @@
       <fieldset>
       <fieldset>
         <legend>{{ t('customize_page.Layout') }}</legend>
         <legend>{{ t('customize_page.Layout') }}</legend>
 
 
+        {% set isLayoutGrowi = 'growi' === settingForm['customize:layout'] || 'crowi-plus' === settingForm['customize:layout'] %}
         <div class="form-group">
         <div class="form-group">
           <div class="col-xs-6">
           <div class="col-xs-6">
             <h4>
             <h4>
-              <input type="radio" name="settingForm[customize:layout]" value="crowi"
-                  {% if !settingForm['customize:layout'] || 'crowi' === settingForm['customize:layout'] %}checked="checked"{% endif %}>
-              Official Crowi Classic Layout
+              <div class="radio radio-primary">
+                <input type="radio" id="radioLayoutGrowi" name="settingForm[customize:layout]" value="growi"
+                    {% if isLayoutGrowi %}checked="checked"{% endif %}>
+                <label for="radioLayoutGrowi">
+                  GROWI Enhanced Layout <small class="text-success">(Recommended)</small>
+                </label>
+              </div>
             </h4>
             </h4>
-            <a href="/images/admin/customize/layout-classic.gif" class="ss-container">
-              <img src="/images/admin/customize/layout-classic-thumb.gif" width="240px">
+            <a href="/images/admin/customize/layout-crowi-plus.gif" class="ss-container">
+              <img src="/images/admin/customize/layout-crowi-plus-thumb.gif" width="240px">
             </a>
             </a>
             <ul>
             <ul>
-              <li>Functional</li>
+              <li>Simple and Clear</li>
               <ul>
               <ul>
-                <li>Collapsible Sidebar</li>
-                <li>Show and post comments in Sidebar</li>
-                <li>Collapsible Table-of-contents</li>
+                <li>Show and post comments from the bottom of the page</li>
+                <li>Affix Table-of-contents</li>
               </ul>
               </ul>
             </ul>
             </ul>
           </div>
           </div>
           <div class="col-xs-6">
           <div class="col-xs-6">
             <h4>
             <h4>
-              <input type="radio" name="settingForm[customize:layout]" value="crowi-plus"
-                  {% if 'crowi-plus' === settingForm['customize:layout'] %}checked="checked"{% endif %}>
-              crowi-plus Enhanced Layout <small class="text-success">(Recommended)</small>
+              <div class="radio radio-primary">
+                <input type="radio" id="radioLayoutCrowi" name="settingForm[customize:layout]" value="crowi"
+                    {% if !isLayoutGrowi %}checked="checked"{% endif %}>
+                <label for="radioLayoutCrowi">
+                  Crowi Classic Layout
+                </label>
+              </div>
             </h4>
             </h4>
-            <a href="/images/admin/customize/layout-crowi-plus.gif" class="ss-container">
-              <img src="/images/admin/customize/layout-crowi-plus-thumb.gif" width="240px">
+            <a href="/images/admin/customize/layout-classic.gif" class="ss-container">
+              <img src="/images/admin/customize/layout-classic-thumb.gif" width="240px">
             </a>
             </a>
             <ul>
             <ul>
-              <li>Simple and Clear</li>
+              <li>Functional</li>
               <ul>
               <ul>
-                <li>Show and post comments from the bottom of the page</li>
-                <li>Affix Table-of-contents</li>
+                <li>Collapsible Sidebar</li>
+                <li>Show and post comments in Sidebar</li>
+                <li>Collapsible Table-of-contents</li>
               </ul>
               </ul>
             </ul>
             </ul>
           </div>
           </div>
@@ -150,13 +203,13 @@
           <label for="settingForm[customize:isEnabledTimeline]" class="col-xs-3 control-label">{{ t('customize_page.Timeline function') }}</label>
           <label for="settingForm[customize:isEnabledTimeline]" class="col-xs-3 control-label">{{ t('customize_page.Timeline function') }}</label>
           <div class="col-xs-9">
           <div class="col-xs-9">
             <div class="btn-group btn-toggle" data-toggle="buttons">
             <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"
                 <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>
-              <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"
                 <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>
               </label>
             </div>
             </div>
 
 
@@ -174,13 +227,13 @@
           <label for="settingForm[customize:isSavedStatesOfTabChanges]" class="col-xs-3 control-label">{{ t("customize_page.tab_switch") }}</label>
           <label for="settingForm[customize:isSavedStatesOfTabChanges]" class="col-xs-3 control-label">{{ t("customize_page.tab_switch") }}</label>
           <div class="col-xs-9">
           <div class="col-xs-9">
             <div class="btn-group btn-toggle" data-toggle="buttons">
             <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"
                 <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>
-              <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"
                 <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>
               </label>
             </div>
             </div>
 
 
@@ -207,7 +260,7 @@
           <div class="form-group">
           <div class="form-group">
             <label for="settingForm[customize:highlightJsStyle]" class="col-xs-3 control-label">{{ t('customize_page.Theme') }}</label>
             <label for="settingForm[customize:highlightJsStyle]" class="col-xs-3 control-label">{{ t('customize_page.Theme') }}</label>
             <div class="col-xs-9">
             <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) %}
                 {% for key in Object.keys(highlightJsCssSelectorOptions) %}
                   <option value={{key}} {% if key == highlightJsStyle() %} selected {% endif %}>{{highlightJsCssSelectorOptions[key].name}}</option>
                   <option value={{key}} {% if key == highlightJsStyle() %} selected {% endif %}>{{highlightJsCssSelectorOptions[key].name}}</option>
                 {% endfor %}
                 {% endfor %}
@@ -219,13 +272,13 @@
             <label for="settingForm[customize:highlightJsStyleBorder]" class="col-xs-3 control-label">(TBD) Border</label>
             <label for="settingForm[customize:highlightJsStyleBorder]" class="col-xs-3 control-label">(TBD) Border</label>
             <div class="col-xs-9">
             <div class="col-xs-9">
               <div class="btn-group btn-toggle" data-toggle="buttons">
               <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"
                   <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>
-                <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"
                   <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>
                 </label>
               </div>
               </div>
             </div>
             </div>
@@ -262,6 +315,37 @@ export  $initHighlight;</code></pre>
         </fieldset>
         </fieldset>
       </form>
       </form>
 
 
+      <form action="/_api/admin/customize/title" method="post" class="form-horizontal" id="customtitleSettingForm" role="form">
+        <fieldset>
+          <legend>カスタム Title</legend>
+
+          <p class="well">
+            <code>&lt;title&gt;</code>タグのコンテンツをカスタマイズできます。<br>
+            <code>&#123;&#123;sitename&#125;&#125;</code>がサイト名、<code>&#123;&#123;page&#125;&#125;</code>がページ名またはページパスに置換されます
+          </p>
+
+          <p class="help-block">
+            Default Value: <code>&#123;&#123;page&#125;&#125; - &#123;&#123;sitename&#125;&#125;</code>
+            <br>
+            Default Output: <pre><code class="xml">&lt;title&gt;/Sandbox - {{ appTitle }}&lt;&#047;title&gt;</code></pre>
+          </p>
+
+          <div class="form-group">
+            <div class="col-xs-12">
+              <input class="form-control" name="settingForm[customize:title]" value="{{ settingForm['customize:title'] }}"></input>
+            </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/header" method="post" class="form-horizontal" id="cutomheaderSettingForm" role="form">
       <form action="/_api/admin/customize/header" method="post" class="form-horizontal" id="cutomheaderSettingForm" role="form">
       <fieldset>
       <fieldset>
         <legend>カスタムヘッダーHTML</legend>
         <legend>カスタムヘッダーHTML</legend>
@@ -292,7 +376,7 @@ export  $initHighlight;</code></pre>
         <div class="form-group">
         <div class="form-group">
           <div class="col-xs-offset-5 col-xs-6">
           <div class="col-xs-offset-5 col-xs-6">
             <input type="hidden" name="_csrf" value="{{ csrf() }}">
             <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>
         </div>
         </div>
 
 
@@ -354,7 +438,7 @@ export  $initHighlight;</code></pre>
             <dt><code>crowiRenderer</code></dt>
             <dt><code>crowiRenderer</code></dt>
             <dd>Crowi Renderer instance</dd>
             <dd>Crowi Renderer instance</dd>
             <dt><code>crowiPlugin</code></dt>
             <dt><code>crowiPlugin</code></dt>
-            <dd>crowi-plus plugin manager instance</dd>
+            <dd>GROWI plugin manager instance</dd>
           </dl>
           </dl>
         </p>
         </p>
         <p class="help-block">
         <p class="help-block">
@@ -391,9 +475,14 @@ window.addEventListener('load', (event) => {
 
 
     </div>
     </div>
   </div>
   </div>
+{% endblock content_main %}
 
 
+{% block body_end %}
+  {% parent %}
   <script>
   <script>
-    $('#cutomcssSettingForm, #cutomscriptSettingForm, #cutomlayoutSettingForm, #cutombehaviorSettingForm, #customfeaturesSettingForm, #cutomheaderSettingForm, #cutomhighlightJsStyleSettingForm').each(function() {
+    $(`#customthemeSettingForm, #cutomlayoutSettingForm, #cutombehaviorSettingForm, #cutomhighlightJsStyleSettingForm,
+       #customfeaturesSettingForm, #cutomheaderSettingForm, #cutomcssSettingForm, #cutomscriptSettingForm, #customtitleSettingForm`
+    ).each(function() {
       $(this).submit(function()
       $(this).submit(function()
       {
       {
         function showMessage(formId, msg, status) {
         function showMessage(formId, msg, status) {
@@ -441,7 +530,9 @@ window.addEventListener('load', (event) => {
       });
       });
     });
     });
 
 
-    // init highlight.js
+    /*
+     * highlight.js style switcher
+     */
     hljs.initHighlightingOnLoad()
     hljs.initHighlightingOnLoad()
 
 
     function selectHighlightJsStyle(event) {
     function selectHighlightJsStyle(event) {
@@ -453,10 +544,31 @@ window.addEventListener('load', (event) => {
       highlightJsCssDOM.href = highlightJsCssDOM.href.replace(/[^/]+\.css$/, `${val}.css`);
       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>
   </script>
 
 
 </div>
 </div>
-{% endblock content_main %}
+{% endblock %}
 
 
 {% block content_footer %}
 {% block content_footer %}
 {% endblock content_footer %}
 {% endblock content_footer %}

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

@@ -1,11 +1,11 @@
 {% extends '../layout/admin.html' %}
 {% extends '../layout/admin.html' %}
 
 
-{% block html_title %}外部アカウント管理 · {% endblock %}
+{% block html_title %}{{ customTitle(t('External Account management')) }}{% endblock %}
 
 
-{% block content_head %}
+{% block content_header %}
 <div class="header-wrap">
 <div class="header-wrap">
   <header id="page-header">
   <header id="page-header">
-    <h1 class="title" id="">ユーザー管理/外部アカウント管理</h1>
+    <h1 class="title" id="">{{ t('User management') }}/{{ t('External Account management') }}</h1>
   </header>
   </header>
 </div>
 </div>
 {% endblock %}
 {% endblock %}
@@ -41,14 +41,14 @@
     <div class="col-md-9">
     <div class="col-md-9">
       <p>
       <p>
         <a class="btn btn-default" href="/admin/users">
         <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>
         </a>
       </p>
       </p>
 
 
       <h2>外部アカウント一覧</h2>
       <h2>外部アカウント一覧</h2>
 
 
-      <table class="table table-hover table-striped table-bordered table-user-list">
+      <table class="table table-bordered table-user-list">
         <thead>
         <thead>
           <tr>
           <tr>
             <th width="120px">Authentication Provider</th>
             <th width="120px">Authentication Provider</th>
@@ -62,12 +62,12 @@
                   data-animation="false" data-html="true"
                   data-animation="false" data-html="true"
                   data-content="<small>関連付けられているユーザーがパスワードを設定しているかどうかを表示します</small>">
                   data-content="<small>関連付けられているユーザーがパスワードを設定しているかどうかを表示します</small>">
                 <small>
                 <small>
-                  <i class="fa fa-info-circle" aria-hidden="true"></i>
+                  <i class="icon-question" aria-hidden="true"></i>
                 </small>
                 </small>
               </a>
               </a>
             </th>
             </th>
-            <th width="100px">作成日</th>
-            <th width="90px">操作</th>
+            <th width="100px">{{ t('user_management.Date created') }}</th>
+            <th width="70px"></th>
           </tr>
           </tr>
         </thead>
         </thead>
         <tbody>
         <tbody>
@@ -96,16 +96,18 @@
               <div class="btn-group admin-user-menu">
               <div class="btn-group admin-user-menu">
 
 
                 <button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown">
                 <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>
                 </button>
                 <ul class="dropdown-menu" role="menu">
                 <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>
                   </li>
                 </ul>{# end of .dropdown-menu #}
                 </ul>{# end of .dropdown-menu #}
 
 

+ 10 - 6
lib/views/admin/index.html

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

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

@@ -1,9 +1,9 @@
 {% extends '../layout/admin.html' %}
 {% extends '../layout/admin.html' %}
 
 
-{% block html_title %}{{ t('Markdown settings') }}
+{% block html_title %}{{ customTitle(t('Markdown settings')) }}
  · {{ path }}{% endblock %}
  · {{ path }}{% endblock %}
 
 
-{% block content_head %}
+{% block content_header %}
 <div class="header-wrap">
 <div class="header-wrap">
   <header id="page-header">
   <header id="page-header">
     <h1 class="title" id="">{{ t('Markdown settings') }}</h1>
     <h1 class="title" id="">{{ t('Markdown settings') }}</h1>
@@ -48,13 +48,13 @@
           </label>
           </label>
           <div class="col-xs-5">
           <div class="col-xs-5">
             <div class="btn-group btn-toggle" data-toggle="buttons">
             <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"
                 <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>
-              <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"
                 <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>
               </label>
             </div>
             </div>
             <p class="help-block">{{ t("markdown_setting.treat_text") }}
             <p class="help-block">{{ t("markdown_setting.treat_text") }}
@@ -68,13 +68,13 @@
           </label>
           </label>
           <div class="col-xs-5">
           <div class="col-xs-5">
             <div class="btn-group btn-toggle" data-toggle="buttons">
             <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"
                 <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>
-              <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"
                 <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>
               </label>
             </div>
             </div>
             <p class="help-block">{{ t("markdown_setting.treat_comment") }}<br>{{ t("markdown_setting.TBD") }}</p>
             <p class="help-block">{{ t("markdown_setting.treat_comment") }}<br>{{ t("markdown_setting.TBD") }}</p>

+ 85 - 79
lib/views/admin/notification.html

@@ -1,8 +1,8 @@
 {% extends '../layout/admin.html' %}
 {% extends '../layout/admin.html' %}
 
 
-{% block html_title %}{{ t('Notification settings') }} · {{ path }}{% endblock %}
+{% block html_title %}{{ customTitle(t('Notification settings')) }}{% endblock %}
 
 
-{% block content_head %}
+{% block content_header %}
 <div class="header-wrap">
 <div class="header-wrap">
   <header id="page-header">
   <header id="page-header">
     <h1 class="title" id="">{{ t('Notification settings') }}</h1>
     <h1 class="title" id="">{{ t('Notification settings') }}</h1>
@@ -38,25 +38,86 @@
 
 
       <ul class="nav nav-tabs" role="tablist">
       <ul class="nav nav-tabs" role="tablist">
         <li class="active">
         <li class="active">
-          <a href="#slack-app" data-toggle="tab" role="tab"><i class="fa fa-slack"></i> Slack App</a>
+          <a href="#slack-incoming-webhooks" data-toggle="tab" role="tab"><i class="icon-settings"></i> Slack Incoming Webhooks</a>
         </li>
         </li>
         <li role="tab">
         <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-app" data-toggle="tab" role="tab"><i class="icon-settings"></i> Slack App</a>
         </li>
         </li>
       </ul>
       </ul>
 
 
-      <div class="tab-content">
-        <div id="slack-app" class="tab-pane active" role="tabpanel" >
+      <div class="tab-content m-t-15">
+        <div id="slack-incoming-webhooks" class="tab-pane active" role="tabpanel">
+
+          <form action="/admin/notification/slackIwhSetting" method="post" class="form-horizontal" id="appSettingForm" role="form">
+            <fieldset>
+              <legend>Slack Incoming Webhooks Configuration</legend>
+
+              <div class="form-group">
+                <label for="slackIwhSetting[slack:incomingWebhookUrl]" class="col-xs-3 control-label">Webhook URL</label>
+                <div class="col-xs-9">
+                  <input class="form-control" type="text" name="slackIwhSetting[slack:incomingWebhookUrl]" value="{{ slackSetting['slack:incomingWebhookUrl'] }}">
+                </div>
+              </div>
+
+              <div class="form-group">
+                <label for="slackIwhSetting[slack:isIncomingWebhookPrioritized]" class="col-xs-3 control-label"></label>
+                <div class="col-xs-9">
+                  <div class="checkbox checkbox-info">
+                    <input type="checkbox" id ="cbPrioritizeIWH" name="slackIwhSetting[slack:isIncomingWebhookPrioritized]" value="1"
+                      {% if slackSetting['slack:isIncomingWebhookPrioritized'] %}checked{% endif %}>
+                    <label for="cbPrioritizeIWH">
+                      Prioritize Incoming Webhook than Slack App
+                    </label>
+                  </div>
+                  <p class="help-block">Check this option and GROWI use Incoming Webhooks even if Slack App settings are enabled.</p>
+                </div>
+              </div>
+
+              <div class="form-group">
+                <div class="col-xs-offset-3 col-xs-6">
+                  <button type="submit" class="btn btn-primary">Submit</button>
+                </div>
+              </div>
+            </fieldset>
+            <input type="hidden" name="_csrf" value="{{ csrf() }}">
+          </form>
+
+          <hr>
+          <h3>
+            <i class="icon-question" aria-hidden="true"></i>
+            <a href="#collapseHelpForIwh" data-toggle="collapse">How to configure Incoming Webhooks?</a>
+          </h3>
+
+          <ol id="collapseHelpForIwh" class="collapse">
+            <li>
+              (At Workspace) Add a hook
+              <ol>
+                <li>Go to <a href="https://slack.com/services/new/incoming-webhook">Incoming Webhooks Configuration page</a>.</li>
+                <li>Choose the default channel to post.</li>
+                <li>Add.</li>
+              </ol>
+            </li>
+            <li>
+              (At GROWI admin page) Set Webhook URL
+              <ol>
+                <li>Input "Webhook URL" and submit on this page.</li>
+              </ol>
+            </li>
+          </ol>
+
+        </div><!-- /#slack-incoming-webhooks -->
+
+        <div id="slack-app" class="tab-pane" role="tabpanel" >
 
 
           <form action="/admin/notification/slackSetting" method="post" class="form-horizontal" id="appSettingForm" role="form">
           <form action="/admin/notification/slackSetting" method="post" class="form-horizontal" id="appSettingForm" role="form">
             <fieldset>
             <fieldset>
               <legend>Slack App Configuration</legend>
               <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>
                 <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>
                 <br><br>
                 Please use <a href="#slack-incoming-webhooks" data-toggle="tab" onclick="activateTab('slack-incoming-webhooks')">Slack incomming webhooks Configuration</a> instead.
                 Please use <a href="#slack-incoming-webhooks" data-toggle="tab" onclick="activateTab('slack-incoming-webhooks')">Slack incomming webhooks Configuration</a> instead.
               </p>
               </p>
@@ -88,16 +149,16 @@
           <div class="text-center">
           <div class="text-center">
             {% if hasSlackToken %}
             {% 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>
             <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>
-            <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>
             </a>
             {% else %}
             {% else %}
             <p>Slack clientId and clientSecret is configured. Now, you can connect with Slack.</p>
             <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>
             </a>
             {% endif %}
             {% endif %}
           </div>
           </div>
@@ -107,7 +168,7 @@
           {# {% if not hasSlackWebClientConfig %} #}
           {# {% if not hasSlackWebClientConfig %} #}
           <hr>
           <hr>
           <h3>
           <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>
             <a href="#collapseHelpForApp" data-toggle="collapse">How to configure Slack App?</a>
           </h3>
           </h3>
 
 
@@ -118,7 +179,7 @@
                 <li>
                 <li>
                   Create App from <a href="https://api.slack.com/applications/new">this link</a>, and fill the form out as below:
                   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">
                   <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>
                     <dt>Development Slack Workspace</dt> <dd>Select the workspace you want to notify to.</dd>
                   </dl>
                   </dl>
                 </li>
                 </li>
@@ -143,7 +204,7 @@
               Set Permission Scopes to the App
               Set Permission Scopes to the App
               <ol>
               <ol>
                 <li>Go to "OAuth &amp; Permissions" page.</li>
                 <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>
                 <li>Don't forget to <strong>save</strong>.</li>
               </ol>
               </ol>
             </li>
             </li>
@@ -162,7 +223,7 @@
             <li>
             <li>
               (At Workspace) Approve the app
               (At Workspace) Approve the app
               <ol>
               <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>
               </ol>
             </li>
             </li>
             <li>
             <li>
@@ -172,10 +233,10 @@
               </ol>
               </ol>
             </li>
             </li>
             <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>
             <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>
             </li>
           </ol>
           </ol>
           {# {% endif %} #}
           {# {% endif %} #}
@@ -183,62 +244,7 @@
 
 
         </div><!-- /#slack-app -->
         </div><!-- /#slack-app -->
 
 
-        <div id="slack-incoming-webhooks" class="tab-pane" role="tabpanel">
 
 
-          <form action="/admin/notification/slackIwhSetting" method="post" class="form-horizontal" id="appSettingForm" role="form">
-            <fieldset>
-              <legend>Slack Incoming Webhooks Configuration</legend>
-
-              <div class="form-group">
-                <label for="slackIwhSetting[slack:incomingWebhookUrl]" class="col-xs-3 control-label">Webhook URL</label>
-                <div class="col-xs-9">
-                  <input class="form-control" type="text" name="slackIwhSetting[slack:incomingWebhookUrl]" value="{{ slackSetting['slack:incomingWebhookUrl'] }}">
-                </div>
-              </div>
-
-              <div class="form-group">
-                <label for="slackIwhSetting[slack:isIncomingWebhookPrioritized]" class="col-xs-3 control-label"></label>
-                <div class="col-xs-9">
-                  <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>
-                </div>
-              </div>
-
-              <div class="form-group">
-                <div class="col-xs-offset-3 col-xs-6">
-                  <button type="submit" class="btn btn-primary">Submit</button>
-                </div>
-              </div>
-            </fieldset>
-            <input type="hidden" name="_csrf" value="{{ csrf() }}">
-          </form>
-
-          <hr>
-          <h3>
-            <i class="fa fa-question-circle" aria-hidden="true"></i>
-            <a href="#collapseHelpForIwh" data-toggle="collapse">How to configure Incoming Webhooks?</a>
-          </h3>
-
-          <ol id="collapseHelpForIwh" class="collapse">
-            <li>
-              (At Workspace) Add a hook
-              <ol>
-                <li>Go to <a href="https://slack.com/services/new/incoming-webhook">Incoming Webhooks Configuration page</a>.</li>
-                <li>Choose the default channel to post.</li>
-                <li>Add.</li>
-              </ol>
-            </li>
-            <li>
-              (At crowi-plus) Set Webhook URL
-              <ol>
-                <li>Input "Webhook URL" and submit on this page.</li>
-              </ol>
-            </li>
-          </ol>
-
-        </div><!-- /#slack-incoming-webhooks -->
 
 
       </div><!-- /.tab-content -->
       </div><!-- /.tab-content -->
 
 
@@ -302,8 +308,8 @@
     window.addEventListener('load', function(e) {
     window.addEventListener('load', function(e) {
       // hash on page
       // hash on page
       if (location.hash) {
       if (location.hash) {
-        if (location.hash == '#slack-incoming-webhooks') {
-          activateTab('slack-incoming-webhooks');
+        if (location.hash == '#slack-app') {
+          activateTab('slack-app');
         }
         }
       }
       }
     });
     });

+ 45 - 40
lib/views/admin/search.html

@@ -1,59 +1,68 @@
 {% extends '../layout/admin.html' %}
 {% extends '../layout/admin.html' %}
 
 
-{% block html_title %}検索管理 · {{ path }}{% endblock %}
+{% block html_title %}{{ customTitle(t('Full Text Search management')) }}{% endblock %}
 
 
-{% block content_head %}
+{% block content_header %}
 <div class="header-wrap">
 <div class="header-wrap">
   <header id="page-header">
   <header id="page-header">
-    <h1 class="title" id="">検索管理</h1>
+    <h1 class="title" id="">{{ t('Full Text Search management') }}</h1>
   </header>
   </header>
 </div>
 </div>
 {% endblock %}
 {% endblock %}
 
 
 {% block content_main %}
 {% block content_main %}
 <div class="content-main">
 <div class="content-main">
+
   <div class="row">
   <div class="row">
     <div class="col-md-3">
     <div class="col-md-3">
       {% include './widget/menu.html' with {current: 'search'} %}
       {% include './widget/menu.html' with {current: 'search'} %}
     </div>
     </div>
     <div class="col-md-9">
     <div class="col-md-9">
 
 
-      {% set smessage = req.flash('successMessage') %}
-      {% if smessage.length %}
-      <div class="alert alert-success">
-        {% for e in smessage %}
-          {{ e }}<br>
-        {% endfor %}
-      </div>
-      {% endif %}
-
-      {% set emessage = req.flash('errorMessage') %}
-      {% if emessage.length %}
-      <div class="alert alert-danger">
-        {% for e in emessage %}
-        {{ e }}<br>
-        {% endfor %}
-      </div>
-      {% endif %}
-
-      <form action="/admin/search/build" method="post" class="form-horizontal" id="appSettingForm" role="form">
-      <fieldset>
-        <legend>Index Build</legend>
-        <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>
-            <p class="help-block">
-              Force rebuild index.<br>
-              Click "Build Now" to delete and create mapping file and add all pages.<br>
-              This may take a while.
-            </p>
+      {% if !searchConfigured() %}
+        <div class="col-md-12">
+          <div class="alert alert-warning">
+            <strong><i class="icon-fw icon-exclamation"></i> Full Text Search is not configured</strong>
           </div>
           </div>
+          <p>Check whether the env var <code>ELASTICSEARCH_URI</code> is set.</p>
         </div>
         </div>
-      </fieldset>
-      <input type="hidden" name="_csrf" value="{{ csrf() }}">
-      </form>
+      {% else %}
+        {% set smessage = req.flash('successMessage') %}
+        {% if smessage.length %}
+        <div class="alert alert-success">
+          {% for e in smessage %}
+            {{ e }}<br>
+          {% endfor %}
+        </div>
+        {% endif %}
 
 
+        {% set emessage = req.flash('errorMessage') %}
+        {% if emessage.length %}
+        <div class="alert alert-danger">
+          {% for e in emessage %}
+          {{ e }}<br>
+          {% endfor %}
+        </div>
+        {% endif %}
+
+        <form action="/admin/search/build" method="post" class="form-horizontal" id="appSettingForm" role="form">
+          <fieldset>
+            <legend>Index Build</legend>
+            <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-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>
+                  This may take a while.
+                </p>
+              </div>
+            </div>
+          </fieldset>
+          <input type="hidden" name="_csrf" value="{{ csrf() }}">
+        </form>
+      {% endif %}
     </div>
     </div>
   </div>
   </div>
 
 
@@ -62,7 +71,3 @@
 
 
 {% block content_footer %}
 {% block content_footer %}
 {% endblock content_footer %}
 {% endblock content_footer %}
-
-
-
-

+ 41 - 33
lib/views/admin/security.html

@@ -1,8 +1,8 @@
 {% extends '../layout/admin.html' %}
 {% extends '../layout/admin.html' %}
 
 
-{% block html_title %}{{ t('Security settings') }} · {% endblock %}
+{% block html_title %}{{ customTitle(t('Security settings')) }} · {% endblock %}
 
 
-{% block content_head %}
+{% block content_header %}
 <div class="header-wrap">
 <div class="header-wrap">
   <header id="page-header">
   <header id="page-header">
     <h1 class="title" id="">{{ t('Security settings') }}</h1>
     <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">
       <form action="/_api/admin/security/general" method="post" class="form-horizontal" id="generalSetting" role="form">
         <fieldset>
         <fieldset>
-        <legend>{{ t('security_setting.Security settings') }}</legend>
+        <legend class="alert-anchor">{{ t('security_setting.Security settings') }}</legend>
 
 
           <div class="form-group">
           <div class="form-group">
             <label for="settingForm[security:registrationMode]" class="col-xs-3 control-label">{{ t('Basic authentication') }}</label>
             <label for="settingForm[security:registrationMode]" class="col-xs-3 control-label">{{ t('Basic authentication') }}</label>
@@ -61,7 +61,7 @@
           <div class="form-group">
           <div class="form-group">
             <label for="settingForm[security:restrictGuestMode]" class="col-xs-3 control-label">{{ t('Guest users access') }}</label>
             <label for="settingForm[security:restrictGuestMode]" class="col-xs-3 control-label">{{ t('Guest users access') }}</label>
             <div class="col-xs-6">
             <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 %}
                 {% for modeValue, modeLabel in consts.restrictGuestMode %}
                 <option value="{{ modeValue }}" {% if modeValue == settingForm['security:restrictGuestMode'] %}selected{% endif %} >{{ modeLabel }}</option>
                 <option value="{{ modeValue }}" {% if modeValue == settingForm['security:restrictGuestMode'] %}selected{% endif %} >{{ modeLabel }}</option>
                 {% endfor %}
                 {% endfor %}
@@ -72,7 +72,7 @@
           <div class="form-group">
           <div class="form-group">
             <label for="settingForm[security:registrationMode]" class="col-xs-3 control-label">{{ t('Register limitation') }}</label>
             <label for="settingForm[security:registrationMode]" class="col-xs-3 control-label">{{ t('Register limitation') }}</label>
             <div class="col-xs-6">
             <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 %}
                 {% for modeValue, modeLabel in consts.registrationMode %}
                 <option value="{{ modeValue }}" {% if modeValue == settingForm['security:registrationMode'] %}selected{% endif %} >{{ modeLabel }}</option>
                 <option value="{{ modeValue }}" {% if modeValue == settingForm['security:registrationMode'] %}selected{% endif %} >{{ modeLabel }}</option>
                 {% endfor %}
                 {% endfor %}
@@ -100,37 +100,45 @@
         </fieldset>
         </fieldset>
       </form>
       </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>
         <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>
           <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="form-group">
             <div class="col-xs-6">
             <div class="col-xs-6">
               <h4>
               <h4>
-                <input type="radio" name="settingForm[security:isEnabledPassport]" value="false"
-                    {% if !settingForm['security:isEnabledPassport'] %}checked="checked"{% endif %}>
-                Official Crowi authentication mechanism
+                <div class="radio radio-primary">
+                  <input type="radio" id="radioPassportAuthMech" name="settingForm[security:isEnabledPassport]" value="true"
+                      {% if true === settingForm['security:isEnabledPassport'] %}checked="checked"{% endif %}>
+                  <label for="radioPassportAuthMech">
+                    <a href="http://passportjs.org/">
+                      <img src="/images/admin/security/passport-logo.svg" class="passport-logo"> Passport
+                    </a> authentication mechanism <small class="text-success">(Recommended)</small>
+                  </label>
+                </div>
               </h4>
               </h4>
               <ul>
               <ul>
                 <li>Username, E-mail and Password authentication</li>
                 <li>Username, E-mail and Password authentication</li>
-                <li>Google OAuth2 authentication</li>
+                <li>LDAP authentication</li>
+                <li class="text-muted">(TBD) <del>Google OAuth2 authentication</del></li>
+                <li class="text-muted">(TBD) <del>Facebook OAuth2 authentication</del></li>
+                <li class="text-muted">(TBD) <del>Twitter OAuth authentication</del></li>
+                <li class="text-muted">(TBD) <del>Github OAuth2 authentication</del></li>
               </ul>
               </ul>
             </div>
             </div>
             <div class="col-xs-6">
             <div class="col-xs-6">
               <h4>
               <h4>
-                <input type="radio" name="settingForm[security:isEnabledPassport]" value="true"
-                    {% if true === settingForm['security:isEnabledPassport'] %}checked="checked"{% endif %}>
-                <a href="http://passportjs.org/">
-                  <img src="/images/admin/security/passport-logo.svg" class="passport-logo"> Passport
-                </a> authentication mechanism <small class="text-success">(Recommended)</small>
+                <div class="radio radio-primary">
+                  <input type="radio" id="radioCrowiAuthMech" name="settingForm[security:isEnabledPassport]" value="false"
+                      {% if !settingForm['security:isEnabledPassport'] %}checked="checked"{% endif %}>
+                  <label for="radioCrowiAuthMech">
+                    Crowi classic authentication mechanism
+                  </label>
+                </div>
               </h4>
               </h4>
               <ul>
               <ul>
                 <li>Username, E-mail and Password authentication</li>
                 <li>Username, E-mail and Password authentication</li>
-                <li>LDAP authentication</li>
-                <li class="text-muted">(TBD) <del>Google OAuth2 authentication</del></li>
-                <li class="text-muted">(TBD) <del>Facebook OAuth2 authentication</del></li>
-                <li class="text-muted">(TBD) <del>Twitter OAuth authentication</del></li>
-                <li class="text-muted">(TBD) <del>Github OAuth2 authentication</del></li>
+                <li>Google OAuth2 authentication</li>
               </ul>
               </ul>
             </div>
             </div>
           </div>
           </div>
@@ -145,7 +153,7 @@
       </form>
       </form>
 
 
 
 
-      <div class="auth-mechanism-configurations">
+      <div class="auth-mechanism-configurations m-t-10">
 
 
         <legend>{{ t('security_setting.Authentication mechanism settings') }}</legend>
         <legend>{{ t('security_setting.Authentication mechanism settings') }}</legend>
 
 
@@ -155,18 +163,18 @@
           <p class="alert alert-warning"
           <p class="alert alert-warning"
               {% if !isRestartingServerNeeded %}style="display: none;"{% endif %}>
               {% if !isRestartingServerNeeded %}style="display: none;"{% endif %}>
             <b>
             <b>
-              <i class="fa fa-exclamation-circle" aria-hidden="true"></i>
+              <i class="icon-exclamation" aria-hidden="true"></i>
               Restarting the server is needed.
               Restarting the server is needed.
             </b>
             </b>
             The server is running with Passport authentication mechanism.
             The server is running with Passport authentication mechanism.
           </p>
           </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 %}>
               {% if isRestartingServerNeeded %}style="opacity: 0.4;"{% endif %}>
 
 
             <fieldset>
             <fieldset>
               <h4>Google 設定</h4>
               <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>
                 Google Cloud Platform の <a href="https://console.cloud.google.com/apis/credentials">API Manager</a>
                 から OAuth2 Client ID を作成すると、Google アカウントにコネクトして登録やログインが可能になります。
                 から OAuth2 Client ID を作成すると、Google アカウントにコネクトして登録やログインが可能になります。
               </p>
               </p>
@@ -217,30 +225,30 @@
           <p class="alert alert-warning"
           <p class="alert alert-warning"
               {% if !isRestartingServerNeeded %}style="display: none;"{% endif %}>
               {% if !isRestartingServerNeeded %}style="display: none;"{% endif %}>
             <b>
             <b>
-              <i class="fa fa-exclamation-circle" aria-hidden="true"></i>
+              <i class="icon-exclamation" aria-hidden="true"></i>
               Restarting the server is needed.
               Restarting the server is needed.
             </b>
             </b>
             The server is running with Official Crowi authentication mechanism.
             The server is running with Official Crowi authentication mechanism.
           </p>
           </p>
           <ul class="nav nav-tabs" role="tablist" {% if isRestartingServerNeeded %}style="opacity: 0.4;"{% endif %}>
           <ul class="nav nav-tabs" role="tablist" {% if isRestartingServerNeeded %}style="opacity: 0.4;"{% endif %}>
             <li class="active">
             <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>
             <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>
             <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>
             <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>
             <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>
             </li>
           </ul>
           </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" >
             <div id="passport-ldap" class="tab-pane active" role="tabpanel" >
               {% include './widget/passport/ldap.html' with { settingForm: settingForm } %}
               {% include './widget/passport/ldap.html' with { settingForm: settingForm } %}
             </div>
             </div>
@@ -282,7 +290,7 @@
           var $message = $('<p class="alert"></p>');
           var $message = $('<p class="alert"></p>');
           $message.addClass('alert-' + status);
           $message.addClass('alert-' + status);
           $message.html(msg.replace('\n', '<br>'));
           $message.html(msg.replace('\n', '<br>'));
-          $message.insertAfter('#' + formId + ' legend');
+          $message.insertAfter('#' + formId + ' .alert-anchor');
 
 
           if (status == 'success') {
           if (status == 'success') {
             setTimeout(function()
             setTimeout(function()

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

@@ -0,0 +1,248 @@
+{% extends '../layout/admin.html' %}
+
+{% block html_title %}{{ customTitle(t('UserGroup management') + '/' + userGroup.name) }}{% endblock %}
+
+{% block content_header %}
+<div class="header-wrap">
+  <header id="page-header">
+    <h1 class="title" id="">{{ t('UserGroup management') + '/' + userGroup.name }}</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 %}
+
+

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

@@ -0,0 +1,174 @@
+{% extends '../layout/admin.html' %}
+
+{% block html_title %}{{ customTitle(t('UserGroup management')) }}{% endblock %}
+
+{% block content_header %}
+<div class="header-wrap">
+  <header id="page-header">
+    <h1 class="title" id="">{{ t('UserGroup management') }}</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="60px">#</th>
+            <th>{{ t('Name') }}</th>
+            <th>ユーザ一覧</th>
+            <th width="100px">作成日</th>
+            <th width="70px"></th>
+          </tr>
+        </thead>
+        <tbody>
+          {% for sGroup in userGroups %}
+          {% set sGroupDetailPageUrl = '/admin/user-group-detail/' + sGroup.name %}
+          <tr>
+            <td>
+              <img src="{{ sGroup|picture }}" class="picture img-circle" />
+            </td>
+            <td><a href="{{ sGroupDetailPageUrl }}">{{ sGroup.name }}</a></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="{{ sGroupDetailPageUrl }}">
+                      <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 %}
+
+

+ 86 - 64
lib/views/admin/users.html

@@ -1,8 +1,8 @@
 {% extends '../layout/admin.html' %}
 {% extends '../layout/admin.html' %}
 
 
-{% block html_title %}{{ t('user_management.User management') }}· {% endblock %}
+{% block html_title %}{{ customTitle(t('user_management.User management')) }}{% endblock %}
 
 
-{% block content_head %}
+{% block content_header %}
 <div class="header-wrap">
 <div class="header-wrap">
   <header id="page-header">
   <header id="page-header">
     <h1 class="title" id="">{{ t('user_management.User management') }}</h1>
     <h1 class="title" id="">{{ t('user_management.User management') }}</h1>
@@ -33,9 +33,11 @@
 
 
     <div class="col-md-9">
     <div class="col-md-9">
       <p>
       <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") }}
           {{ t("user_management.external_account") }}
         </a>
         </a>
       </p>
       </p>
@@ -63,7 +65,7 @@
 
 
             <div class="modal-header">
             <div class="modal-header">
               <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
               <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>
 
 
             <div class="modal-body">
             <div class="modal-body">
@@ -86,7 +88,7 @@
           <div class="modal-content">
           <div class="modal-content">
             <div class="modal-header">
             <div class="modal-header">
               <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
               <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>
 
 
             <div class="modal-body">
             <div class="modal-body">
@@ -117,7 +119,7 @@
 
 
             <div class="modal-header">
             <div class="modal-header">
               <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
               <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>
 
 
             <div class="modal-body">
             <div class="modal-body">
@@ -138,7 +140,7 @@
 
 
       <h2>{{ t("user_management.user_list") }}</h2>
       <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>
         <thead>
           <tr>
           <tr>
             <th width="100px">#</th>
             <th width="100px">#</th>
@@ -148,16 +150,17 @@
             <th>{{ t('Email') }}</th>
             <th>{{ t('Email') }}</th>
             <th width="100px">{{ t('user_management.Date created') }}</th>
             <th width="100px">{{ t('user_management.Date created') }}</th>
             <th width="150px">{{ t('user_management.Last login') }}</th>
             <th width="150px">{{ t('user_management.Last login') }}</th>
-            <th width="90px">{{ t('user_management.Manage') }}</th>
+            <th width="70px"></th>
           </tr>
           </tr>
         </thead>
         </thead>
         <tbody>
         <tbody>
           {% for sUser in users %}
           {% for sUser in users %}
+          {% set sUserId = sUser._id.toString() %}
           <tr>
           <tr>
             <td>
             <td>
-              <img src="{{ sUser|picture }}" class="picture picture-rounded" />
+              <img src="{{ sUser|picture }}" class="picture img-circle" />
               {% if sUser.admin %}
               {% if sUser.admin %}
-              <span class="label label-primary label-admin">
+              <span class="label label-inverse label-admin">
                 Admin
                 Admin
               </span>
               </span>
               {% endif %}
               {% endif %}
@@ -180,22 +183,18 @@
             </td>
             </td>
             <td>
             <td>
               <div class="btn-group admin-user-menu">
               <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>
                 </button>
                 <ul class="dropdown-menu" role="menu">
                 <ul class="dropdown-menu" role="menu">
                   <li class="dropdown-header">{{ t('user_management.Edit menu') }}</li>
                   <li class="dropdown-header">{{ t('user_management.Edit menu') }}</li>
                   <li>
                   <li>
-                    <a href="">{{ t('Edit') }}</a>
-
-                  </li>
-                  <li class="dropdown-button">
                     <a href="#"
                     <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') }}
                       {{ t('user_management.Reissue password') }}
                     </a>
                     </a>
                   </li>
                   </li>
@@ -203,52 +202,65 @@
                   <li class="dropdown-header">{{ t('user_management.Status') }}</li>
                   <li class="dropdown-header">{{ t('user_management.Status') }}</li>
 
 
                   {% if sUser.status == 1 %}
                   {% 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>
                   </li>
                   {% endif  %}
                   {% endif  %}
 
 
                   {% if sUser.status == 2 %}
                   {% 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 %}
                     {% 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 %}
                     {% 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 %}
                     {% endif %}
                   </li>
                   </li>
                   {% endif %}
                   {% endif %}
 
 
                   {% if sUser.status == 3 %}
                   {% 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>
-                  <li class="dropdown-button">
+                  <li>
                     {# label は同じだけど、こっちは論理削除 #}
                     {# 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>
                   </li>
                   {% endif  %}
                   {% endif  %}
 
 
                   {% if sUser.status == 1 || sUser.status == 5 %}
                   {% 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">
                   <li class="dropdown-button">
                     {# label は同じだけど、こっちは物理削除 #}
                     {# 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>
                   </li>
                   {% endif  %}
                   {% endif  %}
 
 
@@ -256,23 +268,33 @@
                   <li class="divider"></li>
                   <li class="divider"></li>
                   <li class="dropdown-header">{{ t('user_management.Administrator menu') }}</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 %}
                     {% 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 %}
                     {% endif %}
                   </li>
                   </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 %}
                   {% endif %}
                 </ul>
                 </ul>
               </div>
               </div>

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

@@ -2,14 +2,13 @@
   {% set current = 'index' %}
   {% set current = 'index' %}
 {% endif  %}
 {% endif  %}
 <ul class="nav nav-pills nav-stacked">
 <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>
-  {% if searchConfigured() %}
-  <li class="{% if current == 'search'%}active{% endif %}"><a href="/admin/search"><i class="fa fa-search"></i> 検索管理</a></li>
-  {% endif %}
+  <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>
+  <li class="{% if current == 'search'%}active{% endif %}"><a href="/admin/search"><i class="icon-fw icon-magnifier"></i> {{ t('Full Text Search management') }}</a></li>
 </ul>
 </ul>

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

@@ -19,7 +19,7 @@
       <li>
       <li>
         Create App from <a href="https://api.slack.com/applications/new">this link</a>, and fill the form out as below:
         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">
         <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>
           <dt>Development Slack Team</dt> <dd>Select the team you want to notify to.</dd>
         </dl>
         </dl>
       </li>
       </li>
@@ -44,7 +44,7 @@
     Set Permission Scopes to the App
     Set Permission Scopes to the App
     <ol>
     <ol>
       <li>Go to "OAuth &amp; Permissions" page.</li>
       <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>
       <li>Don't forget to <strong>save</strong>.</li>
     </ol>
     </ol>
   </li>
   </li>
@@ -63,7 +63,7 @@
   <li>
   <li>
     (At Team) Approve the app
     (At Team) Approve the app
     <ol>
     <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>
     </ol>
   </li>
   </li>
   <li>
   <li>
@@ -73,10 +73,10 @@
     </ol>
     </ol>
   </li>
   </li>
   <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>
   <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>
   </li>
 </ol>
 </ol>
 {% endif %}
 {% 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>
       <label for="{{nameForIsLdapEnabled}}" class="col-xs-3 control-label">Use LDAP</label>
       <div class="col-xs-6">
       <div class="col-xs-6">
         <div class="btn-group btn-toggle" data-toggle="buttons">
         <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"
             <input name="{{nameForIsLdapEnabled}}" value="true" type="radio"
-                {% if true === isLdapEnabled %}checked{% endif %}> Enable
+                {% if true === isLdapEnabled %}checked{% endif %}> ON
           </label>
           </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"
             <input name="{{nameForIsLdapEnabled}}" value="false" type="radio"
-                {% if !isLdapEnabled %}checked{% endif %}> Disable
+                {% if !isLdapEnabled %}checked{% endif %}> OFF
           </label>
           </label>
         </div>
         </div>
       </div>
       </div>
@@ -43,11 +43,11 @@
         <label for="{{nameForIsUserBind}}" class="col-xs-3 control-label">Binding Mode</label>
         <label for="{{nameForIsUserBind}}" class="col-xs-3 control-label">Binding Mode</label>
         <div class="col-xs-6">
         <div class="col-xs-6">
           <div class="btn-group btn-toggle" data-toggle="buttons">
           <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"
               <input name="{{nameForIsUserBind}}" value="false" type="radio"
                   {% if !isUserBind %}checked{% endif %}> Manager Bind
                   {% if !isUserBind %}checked{% endif %}> Manager Bind
             </label>
             </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"
               <input name="{{nameForIsUserBind}}" value="true" type="radio"
                   {% if isUserBind %}checked{% endif %}> User Bind
                   {% if isUserBind %}checked{% endif %}> User Bind
             </label>
             </label>
@@ -284,7 +284,7 @@
 
 
       <div class="modal-header">
       <div class="modal-header">
         <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
         <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>
 
 
       <div class="modal-body">
       <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 %}
 {% else %}
-  {% include '../not_found.html' %}
+  {% include '../layout-growi/not_found.html' %}
 {% endif %}
 {% 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 %}
 {% else %}
-  {% include '../page.html' %}
+  {% include '../layout-growi/page.html' %}
 {% endif %}
 {% 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 %}
 {% else %}
-  {% include '../page_list.html' %}
+  {% include '../layout-growi/page_list.html' %}
 {% endif %}
 {% 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 %}
 {% else %}
-  {% include '../user_page.html' %}
+  {% include '../layout-growi/user_page.html' %}
 {% endif %}
 {% 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_title %}セットアップ {% endblock %}
+{% block html_base_css %}installer nologin{% endblock %}
 
 
-{% block content_main %}
+{% block html_title %}{{ customTitle('セットアップ') }}{% endblock %}
 
 
 
 
-<div class="login-dialog-container col-md-5">
 
 
-<div class="installer-header">
-  <img src="/logo/135x32.png" alt="Crowi">
-  <h1>
-    Crowi のセットアップへようこそ!
-  </h1>
-</div>
+{#
+ # 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">
-    <h2>管理者の作成</h2>
 
 
-    <p class="text-info">
-    はじめに、管理者アカウントを作成してください。
-    </p>
+{% block layout_main %}
 
 
-    {% 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="main container-fluid">
 
 
-    <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>
+  <div class="row">
 
 
-      <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>
+    </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>
       </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>
 <script>
 $(function() {
 $(function() {
   $('#register-form input[name="registerForm[username]"]').change(function(e) {
   $('#register-form input[name="registerForm[username]"]').change(function(e) {
     var username = $(this).val();
     var username = $(this).val();
+    $('#login-dialog').removeClass('has-error');
     $('#input-group-username').removeClass('has-error');
     $('#input-group-username').removeClass('has-error');
     $('#help-block-username').html("");
     $('#help-block-username').html("");
 
 
     $.getJSON('/_api/check_username', {username: username}, function(json) {
     $.getJSON('/_api/check_username', {username: username}, function(json) {
       if (!json.valid) {
       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');
         $('#input-group-username').addClass('has-error');
       }
       }
     });
     });

+ 91 - 75
lib/views/invited.html

@@ -1,111 +1,127 @@
-{% extends 'layout/single-nologin.html' %}
+{% extends 'layout/layout.html' %}
 
 
-{% block html_title %}Registration · {% endblock %}
+{% block html_base_css %}invited nologin{% endblock %}
 
 
-{% block content_main %}
+{% block html_title %}{{ customTitle('Registration') }}{% 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">
-    <h2>ユーザー情報入力</h2>
 
 
-    <p>
-    ようこそ!<br>
-    はじめに、あなたのことを教えて下さい。
-    </p>
 
 
-    <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">
+ {% block layout_main %}
 
 
-      <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="main container-fluid">
 
 
-      <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="row">
 
 
-      <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 class="login-header col-sm-offset-4 col-sm-4">
+      <div class="logo">{% include 'widget/logo.html' %}</div>
+      <h1>GROWI</h1>
+
+      <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>
       </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>
       </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>
 <script>
 $(function() {
 $(function() {
   $('#invited-form input[name="invitedForm[username]"]').change(function(e) {
   $('#invited-form input[name="invitedForm[username]"]').change(function(e) {
     var username = $(this).val();
     var username = $(this).val();
+    $('#login-dialog').removeClass('has-error');
     $('#input-group-username').removeClass('has-error');
     $('#input-group-username').removeClass('has-error');
     $('#help-block-username').html("");
     $('#help-block-username').html("");
 
 
     $.getJSON('/_api/check_username', {username: username}, function(json) {
     $.getJSON('/_api/check_username', {username: username}, function(json) {
       if (!json.valid) {
       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');
         $('#input-group-username').addClass('has-error');
       }
       }
     });
     });
   });
   });
 });
 });
 </script>
 </script>
-</div>
 
 
 {% endblock %}
 {% endblock %}
 
 

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

@@ -0,0 +1,52 @@
+{% extends '../../layout/layout.html' %}
+
+{% block html_title %}{{ customTitle(path) }}{% endblock %}
+
+{% 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">
 <ul class="fitted-list">
   <li class="input-group">
   <li class="input-group">
     <span class="input-group-addon">{{ t('Share Link') }}</span>
     <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>
   <li class="input-group">
   <li class="input-group">
     <span class="input-group-addon">Markdown</span>
     <span class="input-group-addon">Markdown</span>
@@ -10,7 +10,7 @@
   </li>
   </li>
 </ul>
 </ul>
 
 
-<h3><i class="fa fa-comment"></i> Comments</h3>
+<h3><i class="icon-bubble"></i> Comments</h3>
 <div class="page-comments">
 <div class="page-comments">
   <form class="form page-comment-form" id="page-comment-form" onsubmit="return false;">
   <form class="form page-comment-form" id="page-comment-form" onsubmit="return false;">
     <div class="comment-form">
     <div class="comment-form">
@@ -33,11 +33,11 @@
   <div class="page-comments-list" id="page-comments-list">
   <div class="page-comments-list" id="page-comments-list">
     <div class="page-comments-list-newer collapse" id="page-comments-list-newer"></div>
     <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>
     <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 class="page-comments-list-older collapse in" id="page-comments-list-older"></div>
   </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 データが入ってないから。暫定として最新更新ユーザーを表示しちゃう。 #}
     {# default(author) としているのは、v1.1.1 以前に page.creator データが入ってないから。暫定として最新更新ユーザーを表示しちゃう。 #}
     <div class="col-md-3 creator-picture">
     <div class="col-md-3 creator-picture">
       <a href="{{ userPageRoot(page.creator) }}">
       <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>
       </a>
     </div>
     </div>
     <div class="col-md-9">
     <div class="col-md-9">
@@ -15,10 +15,10 @@
         {{ t('Created') }}: {{ page.createdAt|datetz('Y/m/d H:i:s') }}<br>
         {{ t('Created') }}: {{ page.createdAt|datetz('Y/m/d H:i:s') }}<br>
 
 
         {% if page.lastUpdateUser %}
         {% 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 %}
         {% else %}
           {# for BC 1.5.x #}
           {# 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 %}
         {% endif %}
       </p>
       </p>
     </div>
     </div>
@@ -27,7 +27,7 @@
   <div class="like-box">
   <div class="like-box">
     <dl class="dl-horizontal">
     <dl class="dl-horizontal">
       <dt>
       <dt>
-        <i class="fa fa-thumbs-o-up"></i> {{ t('Like!') }}
+        <i class="icon-like"></i> {{ t('Like!') }}
       </dt>
       </dt>
       <dd>
       <dd>
         <p class="liker-count">
         <p class="liker-count">
@@ -36,8 +36,8 @@
         <button
         <button
           data-csrftoken="{{ csrf() }}"
           data-csrftoken="{{ csrf() }}"
           data-liked="{% if page.isLiked(user) %}1{% else %}0{% endif %}"
           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 %}
         {% endif %}
         </p>
         </p>
         <p id="liker-list" class="liker-list" data-likers="{{ page.liker|default([])|join(',') }}">
         <p id="liker-list" class="liker-list" data-likers="{{ page.liker|default([])|join(',') }}">

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

@@ -0,0 +1,32 @@
+{% 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">
   <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">
     <div class="page-comments-list" id="page-comments-list">
       {# transplanted to PageComments React component -- 2017.06.02 Yuki Takei
       {# transplanted to PageComments React component -- 2017.06.02 Yuki Takei
       <div class="page-comments-list-newer collapse" id="page-comments-list-newer"></div>
       <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>
       <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 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;">
     <form class="form page-comment-form" id="page-comment-form" onsubmit="return false;">
       <div class="comment-form">
       <div class="comment-form">
         <div class="comment-form-user">
         <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>
         <div class="comment-form-main">
         <div class="comment-form-main">
           <div class="comment-write" id="comment-write">
           <div class="comment-write" id="comment-write">
@@ -35,7 +35,7 @@
             <input type="hidden" name="commentForm[revision_id]" value="{{ revision._id.toString() }}">
             <input type="hidden" name="commentForm[revision_id]" value="{{ revision._id.toString() }}">
             <div class="pull-right">
             <div class="pull-right">
               <span class="text-danger" id="comment-form-message"></span>
               <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
                 Comment
               </button>
               </button>
             </div>
             </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 %}
 {% block html_additional_headers %}
   {% parent %}
   {% parent %}

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

@@ -4,23 +4,15 @@
 <head>
 <head>
   <meta charset="utf-8">
   <meta charset="utf-8">
   <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
   <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 %}{{ customTitle(path) }}{% endblock %}</title>
   <meta name="description" content="">
   <meta name="description" content="">
   <meta name="author" content="">
   <meta name="author" content="">
 
 
   <meta name="viewport" content="width=device-width,initial-scale=1">
   <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() }}
   {{ customHeader() }}
 
 
@@ -72,22 +64,37 @@ gh/highlightjs/cdn-release@9.12.0/build/languages/yaml.min.js
   {% if env === 'development' %}
   {% if env === 'development' %}
     <script src="/dll/vendor.dll.js"></script>
     <script src="/dll/vendor.dll.js"></script>
     <script src="{{ webpack_asset('dev').js }}" async></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 %}
   {% endif %}
 
 
-  <script src="{{ webpack_asset('style').js }}"></script>
   <script src="{{ webpack_asset('commons').js }}" defer></script>
   <script src="{{ webpack_asset('commons').js }}" defer></script>
   {% if isEnabledPlugins() %}
   {% if isEnabledPlugins() %}
-    <script src="{{ webpack_asset('plugin').js }}" defer></script>
+  <script src="{{ webpack_asset('plugin').js }}" defer></script>
   {% endif %}
   {% endif %}
   {% block html_head_loading_legacy %}
   {% block html_head_loading_legacy %}
-    <script src="{{ webpack_asset('legacy').js }}" defer></script>
+  <script src="{{ webpack_asset('legacy').js }}" defer></script>
   {% endblock %}
   {% endblock %}
+  {% block html_head_loading_app %}
   <script src="{{ webpack_asset('app').js }}" defer></script>
   <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 -->
   <!-- Google Fonts -->
   <link href='https://fonts.googleapis.com/css?family=Lato:400,700' rel='stylesheet' type='text/css'>
   <link href='https://fonts.googleapis.com/css?family=Lato:400,700' rel='stylesheet' type='text/css'>
   <!-- Font Awesome -->
   <!-- Font Awesome -->
   <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css">
   <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 -->
   <!-- emojione -->
   <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/emojione@3.1.2/extras/css/emojione.min.css">
   <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/emojione@3.1.2/extras/css/emojione.min.css">
   <!-- highlight.js -->
   <!-- highlight.js -->
@@ -103,117 +110,126 @@ gh/highlightjs/cdn-release@9.12.0/build/languages/yaml.min.js
 
 
 {% block html_body %}
 {% block html_body %}
 <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-me="{{ user._id.toString() }}"
   data-plugin-enabled="{{ isEnabledPlugins() }}"
   data-plugin-enabled="{{ isEnabledPlugins() }}"
- {% block html_base_attr %}{% endblock %}
+  {% block html_base_attr %}{% endblock %}
   data-csrftoken="{{ csrf() }}"
   data-csrftoken="{{ csrf() }}"
   data-current-username="{% if user %}{{ user.username }}{% endif %}"
   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>
   </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 %}
 {% block body_end %}
 {% endblock %}
 {% endblock %}
-
-{% include '../modal/shortcuts.html' %}
 </body>
 </body>
 {% endblock %}
 {% 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 %}

+ 251 - 206
lib/views/login.html

@@ -1,241 +1,286 @@
-{% extends 'layout/single-nologin.html' %}
+{% extends 'layout/layout.html' %}
 
 
-{% block html_title %}{{ t('Sign in') }} · {% endblock %}
+{% block html_base_css %}login-page nologin{% endblock %}
 
 
-{% block content_main %}
+{% block html_title %}{{ customTitle(t('Sign in')) }}{% 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 flipper {% if req.query.register or req.body.registerForm or isRegistering or googleId %}to-flip{% endif %}" 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">
-    <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 %}
 
 
-      {% set success = req.flash('successMessage') %}
-      {% if success.length %}
-      <div class="alert alert-success">
-        {{ success }}
-      </div>
-      {% endif %}
+{% block layout_main %}
 
 
-      {% 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="main container-fluid">
+
+  <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 %}
         {% 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>
       </div>
-      {% endif %}
     </div>
     </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>
       </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() }}">
           <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>
         </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>
       </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>
     </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>
   </div>
-  {% endif %} {# if registrationMode == Closed #}
 
 
 </div>
 </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 %}
 {% 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 %}{{ customTitle('セットアップ') }}{% 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>
       </div>
-      {% endif %}
 
 
-    {% endif %}
+      <p>{{ reasonMessage }}</p>
+    </div>
 
 
-  </div>
 
 
-</div>
+  </div>{# /.row #}
 
 
-</div>
+</div>{# /.main #}
 
 
 {% endblock %}
 {% endblock %}
-

+ 16 - 13
lib/views/me/api_token.html

@@ -1,12 +1,13 @@
-{% extends '../layout/2column.html' %}
+{% extends '../layout-growi/base/layout.html' %}
 
 
 
 
-{% block html_title %}{{ t('API Settings') }} · {{ path }}{% endblock %}
+{% block html_title %}{{ customTitle(t('API Settings')) }}{% endblock %}
 
 
-{% block content_head %}
+
+{% block content_header %}
 <div class="header-wrap">
 <div class="header-wrap">
   <header id="page-header">
   <header id="page-header">
-  <h1 class="title" id="">{{ t('User Settings') }}</h1>
+  <h1 class="title" id="">{{ t('API Settings') }}</h1>
   </header>
   </header>
 </div>
 </div>
 {% endblock %}
 {% endblock %}
@@ -15,23 +16,25 @@
 <div class="content-main">
 <div class="content-main">
 
 
   <ul class="nav nav-tabs">
   <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>
   </ul>
 
 
   <div class="tab-content">
   <div class="tab-content">
 
 
   {% set message = req.flash('successMessage') %}
   {% set message = req.flash('successMessage') %}
   {% if message.length %}
   {% if message.length %}
-  <div class="alert alert-success">
+  <div class="alert alert-success m-t-10">
     {{ message }}
     {{ message }}
   </div>
   </div>
   {% endif %}
   {% endif %}
 
 
   {% if req.form.errors.length > 0 %}
   {% if req.form.errors.length > 0 %}
-  <div class="alert alert-danger">
+  <div class="alert alert-danger m-t-10">
     <ul>
     <ul>
     {% for error in req.form.errors %}
     {% for error in req.form.errors %}
       <li>{{ error }}</li>
       <li>{{ error }}</li>
@@ -40,7 +43,7 @@
   </div>
   </div>
   {% endif %}
   {% endif %}
 
 
-  <div id="form-box">
+  <div class="form-box m-t-20">
 
 
     <form action="/me/apiToken" method="post" class="form-horizontal" role="form">
     <form action="/me/apiToken" method="post" class="form-horizontal" role="form">
     <fieldset>
     <fieldset>
@@ -59,7 +62,7 @@
       </div>
       </div>
 
 
       <div class="form-group">
       <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">
           <p class="alert alert-warning">
             {{ t('page_me_apitoken.notice.update_token1') }}<br>
             {{ t('page_me_apitoken.notice.update_token1') }}<br>
@@ -82,5 +85,5 @@
 {% block content_footer %}
 {% block content_footer %}
 {% endblock %}
 {% endblock %}
 
 
-{% block footer %}
+{% block layout_footer %}
 {% endblock %}
 {% endblock %}

Некоторые файлы не были показаны из-за большого количества измененных файлов