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

Merge pull request #325 from weseek/rc/3.0.0

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

+ 2 - 1
.eslintrc.js

@@ -31,7 +31,8 @@ module.exports = {
     ],
     ],
     "indent": [
     "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/

+ 10 - 2
CHANGES.md

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

+ 47 - 66
README.md

@@ -1,55 +1,38 @@
-![Crowi](http://res.cloudinary.com/hrscywv4p/image/upload/c_limit,f_auto,h_900,q_80,w_1200/v1/199673/https_www_filepicker_io_api_file_VpYEP32ZQyCZ85u6XCXo_zskpra.png)
+<p align="center">
+  <a href="https://growi.org">
+    <img src="https://user-images.githubusercontent.com/1638767/38254268-d4476bbe-3793-11e8-964c-8865d690baff.png" width="240px">
+  </a>
+</p>
 
 
 <p align="center">
 <p align="center">
-  <a href="https://heroku.com/deploy"><img src="https://www.herokucdn.com/deploy/button.png"></a>
+  <a href="https://demo.growi.org">Demo Site</a>
 </p>
 </p>
 <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://heroku.com/deploy"><img src="https://www.herokucdn.com/deploy/button.png"></a>
+  <a href="https://growi-slackin.weseek.co.jp/"><img src="https://crowi-plus-slackin.weseek.co.jp/badge.svg"></a>
 </p>
 </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)
 [![wercker status](https://app.wercker.com/status/39cdc49d067d65c39cb35d52ceae6dc1/s/master "wercker status")](https://app.wercker.com/project/byKey/39cdc49d067d65c39cb35d52ceae6dc1)
-[![dependencies status](https://david-dm.org/weseek/crowi-plus.svg)](https://david-dm.org/weseek/crowi-plus)
-[![devDependencies Status](https://david-dm.org/weseek/crowi-plus/dev-status.svg)](https://david-dm.org/weseek/crowi-plus?type=dev)
-[![docker pulls](https://img.shields.io/docker/pulls/weseek/crowi-plus.svg)](https://hub.docker.com/r/weseek/crowi-plus/)
-[![MIT License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE)
-
+[![dependencies status](https://david-dm.org/weseek/growi.svg)](https://david-dm.org/weseek/growi)
+[![devDependencies Status](https://david-dm.org/weseek/growi/dev-status.svg)](https://david-dm.org/weseek/growi?type=dev)
+[![docker pulls](https://img.shields.io/docker/pulls/weseek/growi.svg)](https://hub.docker.com/r/weseek/growi/)
 
 
-**crowi-plus** is a fork of [Crowi][crowi] which is [perfectly compatible with the official project](https://github.com/weseek/crowi-plus/wiki/Correspondence-table-with-Crowi-version).
-
-
-Why crowi-plus?
-================
 
 
 * **Pluggable**
 * **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 +47,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 +81,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 +120,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 +154,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 +168,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 +178,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 +199,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 +210,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);

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

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

+ 7 - 4
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",
@@ -75,10 +76,12 @@
 
 
   "Table of Contents": "Table of Contents",
   "Table of Contents": "Table of Contents",
 
 
+  "UserGroup management": "UserGroup management",
   "Public": "Public",
   "Public": "Public",
   "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 +132,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 +145,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"
       }
       }
@@ -250,8 +253,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 +268,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"

+ 6 - 5
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": "ユーザーの基本情報",
@@ -76,14 +77,13 @@
   "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": "ユーザー管理",
+  "UserGroup 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 +91,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 +146,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 +158,7 @@
       "notice": {
       "notice": {
           "version": "これは現在の版ではありません。",
           "version": "これは現在の版ではありません。",
           "moved": "このページは <code>%s</code> から移動しました。",
           "moved": "このページは <code>%s</code> から移動しました。",
+          "duplicated": "このページは <code>%s</code> から複製されました。",
           "unlinked": "このページへのリダイレクトは削除されました。",
           "unlinked": "このページへのリダイレクトは削除されました。",
           "restricted": "このページの閲覧は制限されています"
           "restricted": "このページの閲覧は制限されています"
       }
       }
@@ -267,8 +268,8 @@
     "Plugin settings": "プラグイン設定",
     "Plugin settings": "プラグイン設定",
     "Enable plugin loading": "プラグインの読み込みを有効にします。",
     "Enable plugin loading": "プラグインの読み込みを有効にします。",
     "Load plugins": "プラグインを読み込む",
     "Load plugins": "プラグインを読み込む",
-    "valid": "有効",
-    "invalid": "無効"
+    "Enable": "有効",
+    "Disable": "無効"
 
 
    },
    },
 
 

+ 26 - 8
lib/models/config.js

@@ -21,17 +21,16 @@ module.exports = function(crowi) {
   });
   });
 
 
   /**
   /**
-   * default values when crowi-plus is cleanly installed
+   * default values when GROWI is cleanly installed
    */
    */
   function getArrayForInstalling() {
   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,18 @@ 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)
+  {
+    const key = 'customize:title';
+    return getValueForCrowiNS(config, key);
+  }
+
   configSchema.statics.behaviorType = function(config)
   configSchema.statics.behaviorType = function(config)
   {
   {
     const key = 'customize:behavior';
     const key = 'customize:behavior';
@@ -465,7 +483,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 - 11
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;
@@ -454,15 +456,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 +485,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 +504,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 +784,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 +805,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 +893,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 +924,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 +947,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 +956,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);
+}
+

+ 3 - 2
lib/models/user.js

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

+ 15 - 0
lib/routes/index.js

@@ -74,6 +74,8 @@ module.exports = function(crowi, app) {
   app.post('/_api/admin/customize/css'      , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.customcss, admin.api.customizeSetting);
   app.post('/_api/admin/customize/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);

+ 9 - 5
lib/routes/installer.js

@@ -37,14 +37,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 '/'
           // create portal page for '/'
-          Page.create('/', '# Welcome to crowi-plus!', userData, {});
+          Page.create('/', '# Welcome to GROWI', userData, {});
         });
         });
       });
       });
     } 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) {

+ 44 - 25
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.
@@ -788,6 +802,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 +815,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 +850,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 +866,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],
     };
     };

+ 42 - 3
lib/util/swigFunctions.js

@@ -16,7 +16,7 @@ module.exports = function(crowi, app, req, locals) {
     return crowi.runtimeVersions.versions.yarn ? crowi.runtimeVersions.versions.yarn.version : '-';
     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,21 @@ 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() {
+    var config = crowi.getConfig();
+    var title = Config.customTitle(config);
+    var app_title = Config.appTitle(config);
+    var custom_title = title.replace('{{sitename}}', app_title);
+    return custom_title;
+  }
+
+  locals.behaviorType = function() {
+    var config = crowi.getConfig();
     return Config.behaviorType(config);
     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>

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

@@ -2,7 +2,7 @@
 
 
 {% block html_title %}{{ t('App settings') }} · {% endblock %}
 {% block html_title %}{{ 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>
@@ -38,8 +38,7 @@
         <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.Wiki 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'] }}">
-
+            <input class="form-control" type="text" name="settingForm[app:title]" value="{{ settingForm['app:title'] | default('') }}">
             <p class="help-block">{{ t("app_setting.wiki_change") }}</p>
             <p class="help-block">{{ t("app_setting.wiki_change") }}</p>
           </div>
           </div>
         </div>
         </div>
@@ -133,7 +132,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 +182,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>

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

@@ -2,6 +2,11 @@
 
 
 {% block html_title %}{{ t('Customize') }} {% endblock %}
 {% block html_title %}{{ t('Customize') }} {% endblock %}
 
 
+{% block style_css_block %}
+  <link rel="stylesheet" href="{{ webpack_asset('style').css }}">
+  <link rel="stylesheet" id="jssDefault" {# append id for theme selector #} href="{{ webpack_asset('style-theme-' + theme()).css }}">
+{% endblock %}
+
 {% block html_additional_headers %}
 {% block html_additional_headers %}
   {% parent %}
   {% parent %}
   <!-- CodeMirror -->
   <!-- CodeMirror -->
@@ -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,15 +48,46 @@
     </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"
               <input type="radio" name="settingForm[customize:behavior]" value="crowi"
-                  {% if !settingForm['customize:behavior'] || 'crowi' === settingForm['customize:behavior'] %}checked="checked"{% endif %}>
+                  {% if !isBehaviorGrowi %}checked="checked"{% endif %}>
               Official Crowi Behavior
               Official Crowi Behavior
             </h4>
             </h4>
             <ul>
             <ul>
@@ -67,7 +103,7 @@
           <div class="col-xs-6">
           <div class="col-xs-6">
             <h4>
             <h4>
               <input type="radio" name="settingForm[customize:behavior]" value="crowi-plus"
               <input type="radio" name="settingForm[customize:behavior]" value="crowi-plus"
-                  {% if 'crowi-plus' === settingForm['customize:behavior'] %}checked="checked"{% endif %}>
+                  {% if isBehaviorGrowi %}checked="checked"{% endif %}>
               crowi-plus Simplified Behavior <small class="text-success">(Recommended)</small>
               crowi-plus Simplified Behavior <small class="text-success">(Recommended)</small>
             </h4>
             </h4>
             <ul>
             <ul>
@@ -93,11 +129,12 @@
       <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"
               <input type="radio" name="settingForm[customize:layout]" value="crowi"
-                  {% if !settingForm['customize:layout'] || 'crowi' === settingForm['customize:layout'] %}checked="checked"{% endif %}>
+                  {% if !isLayoutGrowi %}checked="checked"{% endif %}>
               Official Crowi Classic Layout
               Official Crowi Classic Layout
             </h4>
             </h4>
             <a href="/images/admin/customize/layout-classic.gif" class="ss-container">
             <a href="/images/admin/customize/layout-classic.gif" class="ss-container">
@@ -115,7 +152,7 @@
           <div class="col-xs-6">
           <div class="col-xs-6">
             <h4>
             <h4>
               <input type="radio" name="settingForm[customize:layout]" value="crowi-plus"
               <input type="radio" name="settingForm[customize:layout]" value="crowi-plus"
-                  {% if 'crowi-plus' === settingForm['customize:layout'] %}checked="checked"{% endif %}>
+                  {% if isLayoutGrowi %}checked="checked"{% endif %}>
               crowi-plus Enhanced Layout <small class="text-success">(Recommended)</small>
               crowi-plus Enhanced Layout <small class="text-success">(Recommended)</small>
             </h4>
             </h4>
             <a href="/images/admin/customize/layout-crowi-plus.gif" class="ss-container">
             <a href="/images/admin/customize/layout-crowi-plus.gif" class="ss-container">
@@ -150,13 +187,13 @@
           <label for="settingForm[customize:isEnabledTimeline]" class="col-xs-3 control-label">{{ t('customize_page.Timeline function') }}</label>
           <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 +211,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 +244,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 +256,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>
@@ -292,13 +329,55 @@ 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>
 
 
       </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">
+            ヘッダーの&lt;title&gt;タグのコンテンツをカスタムできる。サイト名を入れたい位置に、&#123;&#123;sitename&#125;&#125;
+            パスを入れたい位置に&#123;&#123;path&#125;&#125;を置くことでそれぞれの値に自動置換されます。それ以外の部分は自由に記述して下さい。<br>
+          </p>
+
+          <p class="help-block">
+            Examples:
+            <pre><code>&#123;&#123;sitename&#125;&#125; hoge - &#123;&#123;path&#125;&#125;</code></pre>
+          </p>
+
+          <p class="help-block">
+            Output:
+            <pre><code>&lt;title&gt;GROWI hoge - /xxx/yyy/zzz/Sandbox&lt;&#047;title&gt;</code></pre>
+          </p>
+
+          <div class="form-group">
+            <div class="col-xs-12">
+              <div id="custom-title-editor"></div>
+              <input type="hidden" id="inputCustomTitle" name="settingForm[customize:title]" value="{{ settingForm['customize:title'] }}">
+            </div>
+            <div class="col-xs-12">
+              <p class="help-block text-right">
+                <i class="fa fa-fw fa-keyboard-o" aria-hidden="true"></i>
+                Ctrl+Space でコード補完
+              </p>
+            </div>
+          </div>
+
+          <div class="form-group">
+            <div class="col-xs-offset-5 col-xs-6">
+              <input type="hidden" name="_csrf" value="{{ csrf() }}">
+              <button type="submit" class="btn btn-primary">更新</button>
+            </div>
+          </div>
+
+        </fieldset>
+        </form>
+
       <form action="/_api/admin/customize/css" method="post" class="form-horizontal" id="cutomcssSettingForm" role="form">
       <form action="/_api/admin/customize/css" method="post" class="form-horizontal" id="cutomcssSettingForm" role="form">
       <fieldset>
       <fieldset>
         <legend>{{ t('customize_page.Custom CSS') }}</legend>
         <legend>{{ t('customize_page.Custom CSS') }}</legend>
@@ -354,7 +433,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 +470,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 +525,9 @@ window.addEventListener('load', (event) => {
       });
       });
     });
     });
 
 
-    // init highlight.js
+    /*
+     * highlight.js style switcher
+     */
     hljs.initHighlightingOnLoad()
     hljs.initHighlightingOnLoad()
 
 
     function selectHighlightJsStyle(event) {
     function selectHighlightJsStyle(event) {
@@ -453,10 +539,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 %}

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

@@ -2,7 +2,7 @@
 
 
 {% block html_title %}外部アカウント管理 · {% endblock %}
 {% block html_title %}外部アカウント管理 · {% 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="">ユーザー管理/外部アカウント管理</h1>
@@ -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 #}
 
 

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

@@ -2,7 +2,7 @@
 
 
 {% block html_title %}{{ t('admin_top.Management Wiki') }}· {{ path }}{% endblock %}
 {% block html_title %}{{ t('admin_top.Management Wiki') }}· {{ 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('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') }}

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

@@ -3,7 +3,7 @@
 {% block html_title %}{{ t('Markdown settings') }}
 {% block html_title %}{{ 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>

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

@@ -2,7 +2,7 @@
 
 
 {% block html_title %}{{ t('Notification settings') }} · {{ path }}{% endblock %}
 {% block html_title %}{{ t('Notification settings') }} · {{ 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('Notification settings') }}</h1>
     <h1 class="title" id="">{{ t('Notification settings') }}</h1>
@@ -38,10 +38,10 @@
 
 
       <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-app" data-toggle="tab" role="tab"><i class="icon-settings"></i> Slack App</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-incoming-webhooks" data-toggle="tab" role="tab"><i class="icon-settings"></i> Slack Incoming Webhooks</a>
         </li>
         </li>
       </ul>
       </ul>
 
 
@@ -52,11 +52,11 @@
             <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 +88,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 +107,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 +118,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 +143,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 +162,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 +172,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 %} #}
@@ -202,7 +202,7 @@
                   <input type="checkbox" name="slackIwhSetting[slack:isIncomingWebhookPrioritized]" value="1"
                   <input type="checkbox" name="slackIwhSetting[slack:isIncomingWebhookPrioritized]" value="1"
                     {% if slackSetting['slack:isIncomingWebhookPrioritized'] %}checked{% endif %}>
                     {% if slackSetting['slack:isIncomingWebhookPrioritized'] %}checked{% endif %}>
                   Prioritize Incoming Webhook than Slack App
                   Prioritize Incoming Webhook than Slack App
-                  <p class="help-block">Check this option and crowi-plus use Incoming Webhooks even if Slack App settings are enabled.</p>
+                  <p class="help-block">Check this option and GROWI use Incoming Webhooks even if Slack App settings are enabled.</p>
                 </div>
                 </div>
               </div>
               </div>
 
 
@@ -217,7 +217,7 @@
 
 
           <hr>
           <hr>
           <h3>
           <h3>
-            <i class="fa fa-question-circle" aria-hidden="true"></i>
+            <i class="icon-question" aria-hidden="true"></i>
             <a href="#collapseHelpForIwh" data-toggle="collapse">How to configure Incoming Webhooks?</a>
             <a href="#collapseHelpForIwh" data-toggle="collapse">How to configure Incoming Webhooks?</a>
           </h3>
           </h3>
 
 
@@ -231,7 +231,7 @@
               </ol>
               </ol>
             </li>
             </li>
             <li>
             <li>
-              (At crowi-plus) Set Webhook URL
+              (At GROWI admin page) Set Webhook URL
               <ol>
               <ol>
                 <li>Input "Webhook URL" and submit on this page.</li>
                 <li>Input "Webhook URL" and submit on this page.</li>
               </ol>
               </ol>

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

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

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

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

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

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

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

@@ -2,7 +2,7 @@
 
 
 {% block html_title %}{{ t('user_management.User management') }}· {% endblock %}
 {% block html_title %}{{ 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 - 8
lib/views/admin/widget/menu.html

@@ -2,14 +2,15 @@
   {% 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>
+  <li class="{% if current == 'index'%}active{% endif %}"><a href="/admin"><i class="icon-fw icon-home"></i> {{ t('Management Wiki Home') }}</a></li>
+  <li class="{% if current == 'app'%}active{% endif %}"><a href="/admin/app"><i class="icon-fw icon-settings"></i> {{ t('App settings') }}</a></li>
+  <li class="{% if current == 'security'%}active{% endif %}"><a href="/admin/security"><i class="icon-fw icon-shield"></i> {{ t('Security settings') }}</a></li>
+  <li class="{% if current == 'markdown'%}active{% endif %}"><a href="/admin/markdown"><i class="icon-fw icon-note"></i> {{ t('Markdown settings') }}</a></li>
+  <li class="{% if current == 'customize'%}active{% endif %}"><a href="/admin/customize"><i class="icon-fw icon-wrench"></i> {{ t('Customize') }}</a></li>
+  <li class="{% if current == 'notification'%}active{% endif %}"><a href="/admin/notification"><i class="icon-fw icon-bell"></i> {{ t('Notification settings') }}</a></li>
+  <li class="{% if current == 'user' || current == 'external-account' %}active{% endif %}"><a href="/admin/users"><i class="icon-fw icon-user"></i> {{ t('User management') }}</a></li>
+  <li class="{% if current == 'user-group'%}active{% endif %}"><a href="/admin/user-groups"><i class="icon-fw icon-people"></i> {{ t('UserGroup management') }}</a></li>
   {% if searchConfigured() %}
   {% if searchConfigured() %}
-  <li class="{% if current == 'search'%}active{% endif %}"><a href="/admin/search"><i class="fa fa-search"></i> 検索管理</a></li>
+  <li class="{% if current == 'search'%}active{% endif %}"><a href="/admin/search"><i class="icon-fw icon-magnifier"></i> 検索管理</a></li>
   {% endif %}
   {% endif %}
 </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_base_css %}installer nologin{% endblock %}
 
 
 {% block html_title %}セットアップ {% endblock %}
 {% block html_title %}セットアップ {% endblock %}
 
 
-{% block content_main %}
 
 
 
 
-<div class="login-dialog-container col-md-5">
+{#
+ # Remove default contents
+ #}
+{% block html_head_loading_legacy %}
+{% endblock %}
+{% block html_head_loading_app %}
+{% endblock %}
+{% block layout_head_nav %}
+{% endblock %}
+{% block sidebar %}
+{% endblock %}
 
 
-<div class="installer-header">
-  <img src="/logo/135x32.png" alt="Crowi">
-  <h1>
-    Crowi のセットアップへようこそ!
-  </h1>
-</div>
 
 
 
 
-<div class="login-dialog"  id="login-dialog">
-  <div class="login-dialog-inner">
-    <h2>管理者の作成</h2>
+{% block layout_main %}
 
 
-    <p class="text-info">
-    はじめに、管理者アカウントを作成してください。
-    </p>
+<div class="main container-fluid">
 
 
-    {% if req.form.errors.length > 0 %}
-    <div class="alert alert-danger">
-      <ul>
-      {% for error in req.form.errors %}
-        <li>{{ error }}</li>
-      {% endfor %}
-      </ul>
-    </div>
-    {% endif %}
+  <div class="row">
 
 
-    <form role="form" action="/installer/createAdmin" method="post">
-      <label>ユーザーID</label>
-      <div class="input-group" id="input-group-username">
-        <span class="input-group-addon"><strong>@</strong></span>
-        <input type="text" class="form-control" placeholder="記入例: taroyama" name="registerForm[username]" value="{{ req.body.registerForm.username }}" required>
-      </div>
-      <p class="help-block">
-      <span id="help-block-username" class="text-danger"></span>
-      ユーザーIDは、ユーザーページのURLなどに利用されます。半角英数字と一部の記号のみ利用できます。
-      </p>
-
-      <label>名前</label>
-      <div class="input-group">
-        <span class="input-group-addon"><i class="fa fa-user"></i></span>
-        <input type="text" class="form-control" placeholder="記入例: 山田 太郎" name="registerForm[name]" value="{{ req.body.registerForm.name }}" required>
-      </div>
+    <div class="login-header col-sm-offset-4 col-sm-4">
+      <div class="logo">{% include 'widget/logo.html' %}</div>
+      <h1>GROWI</h1>
 
 
-      <label >メールアドレス</label>
-      <div class="input-group">
-        <span class="input-group-addon"><i class="fa fa-envelope"></i></span>
-        <input type="email" class="form-control" placeholder="E-mail" name="registerForm[email]" value="{{ googleEmail|default(req.body.registerForm.email) }}" required>
+      <div class="login-form-errors">
+        {% if req.form.errors.length > 0 %}
+        <div class="alert alert-danger">
+          <ul>
+          {% for error in req.form.errors %}
+            <li>{{ error }}</li>
+          {% endfor %}
+          </ul>
+        </div>
+        {% endif %}
       </div>
       </div>
+    </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');
       }
       }
     });
     });

+ 90 - 74
lib/views/invited.html

@@ -1,111 +1,127 @@
-{% extends 'layout/single-nologin.html' %}
+{% extends 'layout/layout.html' %}
+
+{% block html_base_css %}invited nologin{% endblock %}
 
 
 {% block html_title %}Registration · {% endblock %}
 {% block html_title %}Registration · {% endblock %}
 
 
-{% block content_main %}
 
 
-<h1 class="login-page">
-  {% if config.crowi['app:title'] == 'Crowi' %}
-    <img src="/logo/135x32.png" alt="Crowi">
-  {% else %}
-    {{ config.crowi['app:title'] }}<br>
-    <img src="/logo/100x11_w.png" alt="powered by Crowi">
-  {% endif %}
-</h1>
 
 
-<div class="login-dialog-container flip-container col-md-5">
+{#
+  # Remove default contents
+  #}
+ {% block html_head_loading_legacy %}
+ {% endblock %}
+ {% block html_head_loading_app %}
+ {% endblock %}
+ {% block layout_head_nav %}
+ {% endblock %}
+ {% block sidebar %}
+ {% endblock %}
 
 
-<div class="login-dialog" id="login-dialog">
 
 
-  <div class="login-dialog-inner front">
-    <h2>ユーザー情報入力</h2>
 
 
-    <p>
-    ようこそ!<br>
-    はじめに、あなたのことを教えて下さい。
-    </p>
+ {% block layout_main %}
 
 
-    <div id="login-form-errors">
-      {% set message = req.flash('warningMessage') %}
-      {% if message.length %}
-      <div class="alert alert-danger">
-        {{ message }}
-      </div>
-      {% endif %}
-
-      {% if req.form.errors.length > 0 %}
-      <div class="alert alert-danger">
-        <ul>
-        {% for error in req.form.errors %}
-          <li>{{ error }}</li>
-        {% endfor %}
-        </ul>
-      </div>
-      {% endif %}
-    </div>
-    <form role="form" id="invited-form" action="/login/activateInvited" method="post">
+<div class="main container-fluid">
 
 
-      <label>メールアドレス</label>
-      <div class="input-group">
-        <span class="input-group-addon"><i class="fa fa-envelope"></i></span>
-        <input type="text" class="form-control" disabled value="{{ user.email }}">
-      </div>
-      <p class="help-block">
-      このメールアドレスで招待を受け取っています。
-      </p>
+  <div class="row">
 
 
-      <label>ユーザーID</label>
-      <div class="input-group" id="input-group-username">
-        <span class="input-group-addon"><strong>@</strong></span>
-        <input type="text" class="form-control" placeholder="記入例: taroyama" name="invitedForm[username]" value="{{ req.body.invitedForm.username }}" required>
-      </div>
-      <p class="help-block">
-      <span id="help-block-username" class="text-danger"></span>
-      ユーザーIDは、ユーザーページのURLなどに利用されます。半角英数字と一部の記号のみ利用できます。
-      </p>
+    <div class="login-header col-sm-offset-4 col-sm-4">
+      <div class="logo">{% include 'widget/logo.html' %}</div>
+      <h1>GROWI</h1>
 
 
-      <label>名前</label>
-      <div class="input-group">
-        <span class="input-group-addon"><i class="fa fa-user"></i></span>
-        <input type="text" class="form-control" placeholder="記入例: 山田 太郎" name="invitedForm[name]" value="{{ req.body.invitedForm.name }}" required>
-      </div>
+      <div id="login-form-errors">
+        {% set message = req.flash('warningMessage') %}
+        {% if message.length %}
+        <div class="alert alert-danger">
+          {{ message }}
+        </div>
+        {% endif %}
 
 
-      <label>パスワード</label>
-      <div class="input-group">
-        <span class="input-group-addon"><i class="fa fa-key"></i></span>
-        <input type="password" class="form-control" placeholder="Password" name="invitedForm[password]" required>
+        {% if req.form.errors.length > 0 %}
+        <div class="alert alert-danger">
+          <ul>
+          {% for error in req.form.errors %}
+            <li>{{ error }}</li>
+          {% endfor %}
+          </ul>
+        </div>
+        {% endif %}
       </div>
       </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 %}
 
 

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

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

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

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

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

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

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

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

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

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

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

@@ -1,8 +1,8 @@
-<h3><i class="fa fa-link"></i> {{ t('Share') }}</h3>
+<h3><i class="icon-link"></i> {{ t('Share') }}</h3>
 <ul class="fitted-list">
 <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(',') }}">

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

@@ -0,0 +1,33 @@
+{% extends '../../layout/layout.html' %}
+
+
+{% block layout_main %}
+<div class="container-fluid">
+
+  <div class="row bg-title">
+    <div class="col-xs-12">
+      {% block content_header %}
+      {% endblock %}
+    </div>
+  </div><!-- /.bg-title -->
+
+  <div class="row">
+    <div id="main" class="main m-t-15 col-md-12 {% if page %}{{ css.grant(page) }}{% endif %} {% block main_css_class %}{% endblock %}">
+      {% block content_main_before %}
+      {% endblock %}
+
+      {% block content_main %}
+      {% endblock content_main %}
+
+      {% block content_main_after %}
+      {% endblock %}
+    </div>
+
+  </div>
+
+</div><!-- /.container-fluid -->
+
+<footer class="footer">
+  {% include '../../widget/system-version.html' %}
+</footer>
+{% endblock %} {# layout_main #}

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

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

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

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

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

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

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

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

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

@@ -2,17 +2,17 @@
 
 
   <div class="page-comments col-lg-7 col-md-9">
   <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 %}{{ path|path2name }} · {{ path }}{% endblock %} {{ appTitle() }}</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 %}

+ 250 - 205
lib/views/login.html

@@ -1,241 +1,286 @@
-{% extends 'layout/single-nologin.html' %}
+{% extends 'layout/layout.html' %}
+
+{% block html_base_css %}login-page nologin{% endblock %}
 
 
 {% block html_title %}{{ t('Sign in') }} · {% endblock %}
 {% block html_title %}{{ t('Sign in') }} · {% endblock %}
 
 
-{% block content_main %}
 
 
-<h1 class="login-page">
-  {% if config.crowi['app:title'] == 'Crowi' %}
-    <img src="/logo/135x32.png" alt="Crowi">
-  {% else %}
-    {{ config.crowi['app:title'] }}<br>
-    <img src="/logo/100x11_w.png" alt="powered by Crowi">
-  {% endif %}
-</h1>
 
 
-<div class="login-dialog-container flip-container col-md-5">
+{#
+ # Remove default contents
+ #}
+{% block html_head_loading_legacy %}
+{% endblock %}
+{% block html_head_loading_app %}
+{% endblock %}
+{% block layout_head_nav %}
+{% endblock %}
+{% block sidebar %}
+{% endblock %}
 
 
-<div class="login-dialog flipper {% if req.query.register or req.body.registerForm or isRegistering or googleId %}to-flip{% endif %}" id="login-dialog">
 
 
-  <div class="login-dialog-inner front">
-    <h2>{{ t('Sign in') }}</h2>
 
 
-    <div id="login-form-errors">
-      {% if isLdapSetupFailed() %}
-      <div class="alert alert-warning">
-        LDAP is enabled but the configuration has something wrong.<br>
-        <small>(set the environment variables <code>DEBUG=crowi:service:PassportService</code> and get the logs)</small>
-      </div>
-      {% endif %}
-
-      {#
-       # The case that there already exists a user whose username matches ID of the newly created LDAP user
-       # https://github.com/weseek/crowi-plus/issues/193
-       #}
-      {% set isDuplicatedUsernameExceptionOccured = req.flash('isDuplicatedUsernameExceptionOccured') %}
-      {% if isDuplicatedUsernameExceptionOccured != null %}
-      <div class="alert alert-warning">
-        <i class="fa fa-fw fa-info-circle"></i>
-        <strong>DuplicatedUsernameException occured</strong>
-        <p>
-          Your LDAP authentication was succeess, but a new user could not be created.
-          See the issue <a href="https://github.com/weseek/crowi-plus/issues/193">#193</a>.
-        </p>
-      </div>
-      {% endif %}
+{% block layout_main %}
 
 
-      {% set success = req.flash('successMessage') %}
-      {% if success.length %}
-      <div class="alert alert-success">
-        {{ success }}
-      </div>
-      {% endif %}
+<div class="main container-fluid">
 
 
-      {% set warn = req.flash('warningMessage') %}
-      {% if warn.length %}
-      {% for w in warn %}
-      <div class="alert alert-warning">
-        {{ w }}
-      </div>
-      {% endfor %}
-      {% endif %}
-
-      {% set error = req.flash('errorMessage') %}
-      {% if error.length %}
-      {% for e in error %}
-      <div class="alert alert-danger">
-        {{ e }}
-      </div>
-      {% endfor %}
-      {% endif %}
-
-      {% if req.form.errors.length > 0 %}
-      <div class="alert alert-danger">
-        <ul>
-        {% for error in req.form.errors %}
-          <li>{{ error }}</li>
-        {% endfor %}
-        </ul>
-      </div>
-      {% endif %}
-    </div>
-    <form role="form" action="/login" method="post">
-      <div class="input-group">
-        <span class="input-group-addon"><i class="fa fa-fw fa-user"></i></span>
-        <input type="text" class="form-control" placeholder="Username or E-mail" name="loginForm[username]">
-        {% if isLdapSetup() %}
-        <span class="input-group-addon">
-          <small class="text-primary">
-            <i class="fa fa-fw fa-check-circle"></i> LDAP
-          </small>
-        </span>
+  <div class="row">
+    <div class="login-header col-sm-offset-4 col-sm-4">
+      <div class="logo">{% include 'widget/logo.html' %}</div>
+      <h1>{{ appTitle() }}</h1>
+
+      <div class="login-form-errors">
+        {% if isLdapSetupFailed() %}
+        <div class="alert alert-warning small">
+          <strong><i class="icon-fw icon-info"></i>LDAP is enabled but the configuration has something wrong.</strong>
+          <br>
+          (Please set the environment variables <code>DEBUG=crowi:service:PassportService</code> to get the logs)
+        </div>
         {% endif %}
         {% 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 %}セットアップ {% 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 %}
-

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

@@ -1,12 +1,12 @@
-{% extends '../layout/2column.html' %}
+{% extends '../layout-growi/base/layout.html' %}
 
 
 
 
 {% block html_title %}{{ t('API Settings') }} · {{ path }}{% endblock %}
 {% block html_title %}{{ t('API Settings') }} · {{ 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('User Settings') }}</h1>
+  <h1 class="title" id="">{{ t('API Settings') }}</h1>
   </header>
   </header>
 </div>
 </div>
 {% endblock %}
 {% endblock %}
@@ -15,10 +15,12 @@
 <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">
@@ -40,7 +42,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 +61,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 +84,5 @@
 {% block content_footer %}
 {% block content_footer %}
 {% endblock %}
 {% endblock %}
 
 
-{% block footer %}
+{% block layout_footer %}
 {% endblock %}
 {% endblock %}

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

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

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

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

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

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

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

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

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

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

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

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

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