فهرست منبع

Merge branch 'master' into inprv/better-input-on-mobile

# Conflicts:
#	resource/js/components/PageEditor/Editor.js
#	resource/styles/scss/_on-edit.scss
Yuki Takei 7 سال پیش
والد
کامیت
f45cb3a0f5

+ 3 - 1
CHANGES.md

@@ -4,7 +4,9 @@ CHANGES
 ## 3.1.0-RC
 
 * Improvement: Group Access Control List - Select group modal
-* Improvement: Auto-format markdown tables which includes multibyte text
+* Improvement: Add 'future' theme
+* Improvement: Auto-format markdown table which includes multibyte text
+* Improvement: Show icon when auto-format markdown table is activated
 * Improvement: Enable to switch show/hide border for highlight.js
 * Improvement: BindDN field allows also ActiveDirectory styles 
 * Improvement: Show LDAP logs when testing login

+ 1 - 0
config/webpack.common.js

@@ -30,6 +30,7 @@ module.exports = function(options) {
       'style-theme-default-dark':  './resource/styles/scss/theme/default-dark.scss',
       'style-theme-nature':   './resource/styles/scss/theme/nature.scss',
       'style-theme-mono-blue':   './resource/styles/scss/theme/mono-blue.scss',
+      'style-theme-future': './resource/styles/scss/theme/future.scss',
       'style-presentation':   './resource/styles/scss/style-presentation.scss',
     },
     externals: {

+ 1 - 0
lib/form/revision.js

@@ -8,5 +8,6 @@ module.exports = form(
   field('pageForm.body').required().custom(function(value) { return value.replace(/\r/g, '\n') }),
   field('pageForm.currentRevision'),
   field('pageForm.grant').toInt().required(),
+  field('pageForm.grantUserGroupId'),
   field('pageForm.notify')
 );

+ 1 - 0
lib/locales/en-US/translation.json

@@ -86,6 +86,7 @@
   "Specified users only": "Specified users only",
   "Just me": "Just me",
   "Only inside the group": "Only inside the group",
+  "Reselect the group": "Reselect the group",
   "Shareable link": "Shareable link",
 
   "Show latest": "Show latest",

+ 1 - 0
lib/locales/ja/translation.json

@@ -100,6 +100,7 @@
   "Specified users": "特定ユーザーのみ",
   "Just me": "自分のみ",
   "Only inside the group": "特定グループのみ",
+  "Reselect the group": "グループの再選択",
   "Shareable link": "このページの共有用URL",
   "The whitelist of registration permission E-mail address": "登録許可メールアドレスの<br>ホワイトリスト",
   "Selecting authentication mechanism": "認証機構選択",

+ 18 - 52
lib/models/page-group-relation.js

@@ -88,21 +88,18 @@ class PageGroupRelation {
    * @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);
-      });
-  }
+  // 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);
+  // }
 
   /**
    * find the relation or create(if not exists) for page and group
@@ -126,10 +123,6 @@ class PageGroupRelation {
         else {
           return this.createRelation(userGroup, page);
         }
-      })
-      .catch((err) => {
-        debug('An Error occured.', err);
-        return reject(err);
       });
   }
 
@@ -147,7 +140,7 @@ class PageGroupRelation {
       return null;
     }
     return this
-      .find({ targetPage: page.id })
+      .findOne({ targetPage: page.id })
       .populate('relatedGroup')
       .exec();
   }
@@ -165,25 +158,8 @@ class PageGroupRelation {
     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);
+      .then(pageRelation => {
+        return UserGroupRelation.isRelatedUserForGroup(userData, pageRelation.relatedGroup);
       });
   }
 
@@ -237,15 +213,9 @@ class 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();
-          });
+      .then(relation => {
+        if (relation != null) {
+          relation.remove();
         }
       });
   }
@@ -268,10 +238,6 @@ class PageGroupRelation {
         else {
           relationData.remove();
         }
-      })
-      .catch((err) => {
-        debug('Error on find a removing page-group-relation', err);
-        return reject(err);
       });
   }
 }

+ 15 - 24
lib/models/page.js

@@ -507,8 +507,8 @@ module.exports = function(crowi) {
 
         if (!pageData.isGrantedFor(userData)) {
           PageGroupRelation.isExistsGrantedGroupForPageAndUser(pageData, userData)
-            .then(function(checkResult) {
-              if (!checkResult) {
+            .then(isExists => {
+              if (!isExists) {
                 return reject(new Error('Page is not granted for the user')); //PAGE_GRANT_ERROR, null);
               }
               else {
@@ -801,10 +801,11 @@ module.exports = function(crowi) {
   pageSchema.statics.updateGrant = function(page, grant, userData, grantUserGroupId) {
     var Page = this;
 
-    if (grant == GRANT_USER_GROUP && grantUserGroupId == null) {
-      throw new Error('grant userGroupId is not specified');
-    }
     return new Promise(function(resolve, reject) {
+      if (grant == GRANT_USER_GROUP && grantUserGroupId == null) {
+        reject('grant userGroupId is not specified');
+      }
+
       page.grant = grant;
       if (grant == GRANT_PUBLIC || grant == GRANT_USER_GROUP) {
         page.grantedUsers = [];
@@ -838,12 +839,12 @@ module.exports = function(crowi) {
       return UserGroupRelation.findByGroupIdAndUser(grantUserGroupId, userData)
       .then((relation) => {
         if (relation == null) {
-          return reject(new Error('no relations were exist for group and user.'));
+          return 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));
+        return new Error('No UserGroup is exists. userGroupId : ', grantUserGroupId);
       });
     }
     else {
@@ -967,25 +968,15 @@ module.exports = function(crowi) {
     // update existing page
     var newRevision = Revision.prepareRevision(pageData, body, user);
 
-    return new Promise(function(resolve, reject) {
-      Page.pushRevision(pageData, newRevision, user)
+    return Page.pushRevision(pageData, newRevision, user)
       .then(function(revision) {
-        if (grant != pageData.grant) {
-          return Page.updateGrant(pageData, grant, user, grantUserGroupId).then(function(data) {
-            debug('Page grant update:', data);
-            resolve(data);
-            pageEvent.emit('update', data, user);
-          });
-        }
-        else {
-          resolve(pageData);
-          pageEvent.emit('update', pageData, user);
-        }
-      }).catch(function(err) {
-        debug('Error on update', err);
-        debug('Error on update', err.stack);
+        return Page.updateGrant(pageData, grant, user, grantUserGroupId);
+      })
+      .then(function(data) {
+        debug('Page grant update:', data);
+        pageEvent.emit('update', data, user);
+        return data;
       });
-    });
   };
 
   pageSchema.statics.deletePage = function(pageData, user, options) {

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

@@ -66,8 +66,6 @@ class UserGroupRelation {
    */
   static findAllRelationForUserGroup(userGroup) {
     debug('findAllRelationForUserGroup is called', userGroup);
-    var UserGroupRelation = this;
-
     return this
       .find({ relatedGroup: userGroup })
       .populate('relatedUser')
@@ -83,7 +81,6 @@ class UserGroupRelation {
    * @memberof UserGroupRelation
    */
   static findAllRelationForUserGroups(userGroups) {
-
     return this
       .find({ relatedGroup: { $in: userGroups } })
       .populate('relatedUser')
@@ -99,7 +96,6 @@ class UserGroupRelation {
    * @memberof UserGroupRelation
    */
   static findAllRelationForUser(user) {
-
     return this
       .find({ relatedUser: user.id })
       .populate('relatedGroup')
@@ -199,10 +195,6 @@ class UserGroupRelation {
       .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);
       });
   }
 
@@ -263,10 +255,6 @@ class UserGroupRelation {
         else {
           relationData.remove();
         }
-      })
-      .catch((err) => {
-        debug('Error on find a removing user-group-relation', err);
-        reject(err);
       });
   }
 

+ 4 - 3
lib/routes/index.js

@@ -148,9 +148,10 @@ module.exports = function(crowi, app) {
   app.get( '/_search'                 , loginRequired(crowi, app, false) , search.searchPage);
   app.get( '/_api/search'             , accessTokenParser , loginRequired(crowi, app, false) , search.api.search);
 
-  app.get( '/_api/check_username'     , user.api.checkUsername);
-  app.post('/_api/me/picture/upload'  , loginRequired(crowi, app) , uploads.single('userPicture'), me.api.uploadPicture);
-  app.get( '/_api/user/bookmarks'     , loginRequired(crowi, app, false) , user.api.bookmarks);
+  app.get( '/_api/check_username'           , user.api.checkUsername);
+  app.post('/_api/me/picture/upload'        , loginRequired(crowi, app) , uploads.single('userPicture'), me.api.uploadPicture);
+  app.get( '/_api/me/user-group-relations'  , accessTokenParser , loginRequired(crowi, app) , me.api.userGroupRelations);
+  app.get( '/_api/user/bookmarks'           , loginRequired(crowi, app, false) , user.api.bookmarks);
 
   // HTTP RPC Styled API (に徐々に移行していいこうと思う)
   app.get('/_api/users.list'          , accessTokenParser , loginRequired(crowi, app, false) , user.api.list);

+ 14 - 2
lib/routes/me.js

@@ -5,10 +5,10 @@ module.exports = function(crowi, app) {
     , fs = require('fs')
     , models = crowi.models
     , config = crowi.getConfig()
-    , Page = models.Page
     , User = models.User
+    , UserGroupRelation = models.UserGroupRelation
     , ExternalAccount = models.ExternalAccount
-    , Revision = models.Revision
+    , ApiResponse = require('../util/apiResponse')
     //, pluginService = require('../service/plugin')
     , actions = {}
     , api = {}
@@ -76,6 +76,18 @@ module.exports = function(crowi, app) {
     });
   };
 
+  /**
+   * retrieve user-group-relation documents
+   * @param {object} req
+   * @param {object} res
+   */
+  api.userGroupRelations = function(req, res) {
+    UserGroupRelation.findAllRelationForUser(req.user)
+      .then(userGroupRelations => {
+        return res.json(ApiResponse.success({userGroupRelations}));
+      });
+  };
+
   actions.index = function(req, res) {
     var userForm = req.body.userForm;
     var userData = req.user;

+ 65 - 70
lib/routes/page.js

@@ -181,11 +181,13 @@ module.exports = function(crowi, app) {
       else {
         return Promise.resolve([]);
       }
-    }).then(function(tree) {
+    })
+    .then(function(tree) {
       renderVars.tree = tree;
 
       return Page.findListByStartWith(path, req.user, queryOptions);
-    }).then(function(pageList) {
+    })
+    .then(function(pageList) {
 
       if (pageList.length > limit) {
         pageList.pop();
@@ -198,6 +200,16 @@ module.exports = function(crowi, app) {
       };
       renderVars.pager = generatePager(pagerOptions);
       renderVars.pages = pagePathUtil.encodePagesPath(pageList);
+    })
+    .then(() => {
+      return PageGroupRelation.findByPage(renderVars.page);
+    })
+    .then((pageGroupRelation) => {
+      if (pageGroupRelation != null) {
+        renderVars.pageRelatedGroup = pageGroupRelation.relatedGroup;
+      }
+    })
+    .then(() => {
       res.render('customlayout-selector/page_list', renderVars);
     }).catch(function(err) {
       debug('Error on rendering pageListShow', err);
@@ -238,7 +250,6 @@ module.exports = function(crowi, app) {
       author: false,
       pages: [],
       tree: [],
-      userRelatedGroups: [],
       pageRelatedGroup: null,
     };
 
@@ -266,8 +277,16 @@ module.exports = function(crowi, app) {
         return Revision.findRevisionList(page.path, {})
         .then(function(tree) {
           renderVars.tree = tree;
-          return Promise.resolve();
-        }).then(function() {
+        })
+        .then(() => {
+          return PageGroupRelation.findByPage(renderVars.page);
+        })
+        .then((pageGroupRelation) => {
+          if (pageGroupRelation != null) {
+            renderVars.pageRelatedGroup = pageGroupRelation.relatedGroup;
+          }
+        })
+        .then(function() {
           var userPage = isUserPage(page.path);
           var userData = null;
 
@@ -296,14 +315,8 @@ module.exports = function(crowi, app) {
               // pass
             });
           }
-          else {
-            return Promise.resolve();
-          }
         });
       }
-      else {
-        return Promise.resolve();
-      }
     })
     // page not exists
     .catch(function(err) {
@@ -315,47 +328,32 @@ module.exports = function(crowi, app) {
     .then(function() {
       if (!isRedirect) {
         Page.findListWithDescendants(path, req.user, queryOptions)
-        .then(function(pageList) {
-          if (pageList.length > limit) {
-            pageList.pop();
-          }
-
-          pagerOptions.length = pageList.length;
-
-          renderVars.viewConfig = {
-            seener_threshold: SEENER_THRESHOLD,
-          };
-          renderVars.pager = generatePager(pagerOptions);
-          renderVars.pages = pagePathUtil.encodePagesPath(pageList);
-
-          return Promise.resolve();
-        })
-        .then(function() {
-          return interceptorManager.process('beforeRenderPage', req, res, renderVars);
-        })
-        .then(function() {
-          res.render(req.query.presentation ? 'page_presentation' : pageTeamplate, renderVars);
-        })
-        .catch(function(err) {
-          console.log(err);
-          debug('Error on rendering pageListShowForCrowiPlus', err);
-        });
-      }
-    })
-    .then(function() {
-      return UserGroupRelation.findAllRelationForUser(req.user);
-    }).then(function(groupRelations) {
-      if (groupRelations != null) {
-        renderVars.userRelatedGroups = groupRelations.map(relation => relation.relatedGroup);
-      }
-
-      return PageGroupRelation.findByPage(renderVars.page);
-    }).then((pageGroupRelation) => {
-      if (pageGroupRelation != null) {
-        renderVars.pageRelatedGroup = pageGroupRelation.relatedGroup;
+          .then(function(pageList) {
+            if (pageList.length > limit) {
+              pageList.pop();
+            }
+
+            pagerOptions.length = pageList.length;
+
+            renderVars.viewConfig = {
+              seener_threshold: SEENER_THRESHOLD,
+            };
+            renderVars.pager = generatePager(pagerOptions);
+            renderVars.pages = pagePathUtil.encodePagesPath(pageList);
+
+            return;
+          })
+          .then(function() {
+            return interceptorManager.process('beforeRenderPage', req, res, renderVars);
+          })
+          .then(function() {
+            res.render(req.query.presentation ? 'page_presentation' : pageTeamplate, renderVars);
+          })
+          .catch(function(err) {
+            console.log(err);
+            debug('Error on rendering pageListShowForCrowiPlus', err);
+          });
       }
-
-      return Promise.resolve();
     });
   };
 
@@ -431,19 +429,10 @@ module.exports = function(crowi, app) {
   function renderPage(pageData, req, res) {
     // create page
     if (!pageData) {
-      var userRelatedGroups
-      UserGroupRelation.findAllRelationForUser(req.user)
-        .then((groupRelations) => {
-          userRelatedGroups = groupRelations.map(relation => relation.relatedGroup);
-          return Promise.resolve();
-        }).then(() => {
-          debug('not found page user group resolver : ', userRelatedGroups);
-          return res.render('customlayout-selector/not_found', {
-            author: {},
-            page: false,
-            userRelatedGroups: userRelatedGroups,
-          });
-        });
+      return res.render('customlayout-selector/not_found', {
+        author: {},
+        page: false,
+      });
     }
 
     if (pageData.redirectTo) {
@@ -462,9 +451,16 @@ module.exports = function(crowi, app) {
     Revision.findRevisionList(pageData.path, {})
     .then(function(tree) {
       renderVars.tree = tree;
-
-      return Promise.resolve();
-    }).then(function() {
+    })
+    .then(() => {
+      return PageGroupRelation.findByPage(renderVars.page);
+    })
+    .then((pageGroupRelation) => {
+      if (pageGroupRelation != null) {
+        renderVars.pageRelatedGroup = pageGroupRelation.relatedGroup;
+      }
+    })
+    .then(function() {
       if (userPage) {
         return User.findUserByUsername(User.getUsernameByPath(pageData.path))
         .then(function(data) {
@@ -508,7 +504,6 @@ module.exports = function(crowi, app) {
 
   actions.pageShow = function(req, res) {
     var path = path || getPathFromRequest(req);
-    var options = {};
 
     // FIXME: せっかく getPathFromRequest になってるのにここが生 params[0] だとダサイ
     var isMarkdown = req.params[0].match(/.+\.md$/) || false;
@@ -623,12 +618,12 @@ module.exports = function(crowi, app) {
 
       if (data) {
         previousRevision = data.revision;
-        return Page.updatePage(data, body, req.user, { grant: grant, grantUserGroupId: grantUserGroupId});
+        return Page.updatePage(data, body, req.user, { grant, grantUserGroupId });
       }
       else {
         // new page
         updateOrCreate = 'create';
-        return Page.create(path, body, req.user, { grant: grant, grantUserGroupId: grantUserGroupId});
+        return Page.create(path, body, req.user, { grant, grantUserGroupId });
       }
     }).then(function(data) {
       // data is a saved page data.

+ 5 - 44
lib/views/_form.html

@@ -46,54 +46,15 @@
       </span>
       {% endif %}
 
-      {% if forceGrant %}
-      <input type="hidden" name="pageForm[grant]" value="{{ forceGrant }}">
-      {% else %}
-      <div>
-        <div id="page-grant-selector"></div>
-      </div>
-      <input type="hidden" id="page-grant" name="pageForm[grant]" value="{{ pageForm.grant|default(page.grant) }}">
-      <input id="grant-group" type="hidden" value="{% if pageForm.grant %}{{ pageForm.grant }}{% endif %}">
-<!--
-      <select id="select-grant" name="pageForm[grant]" class="m-r-5 selectpicker btn-group-sm">
-        {% for grantId, grantLabel in consts.pageGrants %}
-        <option value="{{ grantId }}" {% if pageForm.grant|default(page.grant) == grantId %}selected{% endif %}>{{ t(grantLabel) }}</option>
-        {% endfor %}
-        {% if user and user.admin && userRelatedGroups %}
-        <option id="no-group" value="/admin/user-groups">{{ t('Only inside the group') }} you have no groups.</option>
-        {% endif %}
-        <option id="group-grant" value="5">{{ t('Only inside the group') }}</option>
-        {% if pageForm.grant|default(page.grant) == "5" && pageRelatedGroup != null %}
-        <option id="group-grant" value="5" selected>{{pageRelatedGroup}}</option>
-        {% endif %}
-      </select>
-      <input id="select-grant-pre" type="hidden" value="{{ page.grant }}">
-      {% endif %}
-      <input id="grant-group" type="hidden" name="pageForm[grantUserGroupId]" value="">
-      {% if userRelatedGroups.length != 0 %}
-      <div class="collapse width">
-        <select name="pageForm[grantUserGroupId]" class="selectpicker btn-group-sm">
-          {% for userGroup in userRelatedGroups %}
-          <option value="{{ userGroup.id }}">{{ userGroup.name }}</option>
-          {% endfor %}
-        </select>
-      </div>
-      {% endif %} -->
-      {% if userRelatedGroups.length != 0 %}
-      <div>
-        <select name="pageForm[grantUserGroupId]" class="selectpicker btn-group-sm">
-          {% for userGroup in userRelatedGroups %}
-          <option value="{{ userGroup.id }}">{{ userGroup.name }}</option>
-          {% endfor %}
-        </select>
-      </div>
-      {% endif %}
-      <!-- <input type="hidden" id="page-grant" value="{{ page.grant }}"> -->
-      <input type="hidden" id="user-related-group-data" value="{{userRelatedGroups}}">
+      <div id="page-grant-selector"></div>
+
+      <input type="hidden" id="page-grant" name="pageForm[grant]" value="{{ page.grant }}">
+      <input type="hidden" id="grant-group" name="pageForm[grantUserGroupId]" value="{{ pageRelatedGroup._id.toString() }}">
       <input type="hidden" id="edit-form-csrf" name="_csrf" value="{{ csrf() }}">
       <button type="submit" class="btn btn-primary btn-submit" id="edit-form-submit">{{ t('Update') }}</button>
     </div>
   </div>
 </form>
+<input type="hidden" id="grant-group-name" value="{{ pageRelatedGroup.name }}">{# for storing group name #}
 <div class="file-module hidden">
 </div>

+ 1 - 0
lib/views/admin/customize.html

@@ -73,6 +73,7 @@
             {# Dark Themes #}
             <div class="d-flex">
               {% include 'widget/theme-colorbox.html' with { name: 'default-dark', bg: '#212731', topbar: '#151515', theme: '#f75b36' } %}
+              {% include 'widget/theme-colorbox.html' with { name: 'future', bg: '#16282D', topbar: '#011414', theme: '#04B4AE' } %}
             </div>
           </div>
 

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

@@ -25,6 +25,5 @@
   </div>
 
   <div id="crowi-modals">
-    {% include '../modal/select_grant_group.html' %}
   </div>
 {% endblock %}

+ 1 - 0
lib/views/layout/layout.html

@@ -118,6 +118,7 @@ gh/highlightjs/cdn-release@9.12.0/build/languages/yaml.min.js
   class="main-container content-wrapper {% block html_base_css %}{% endblock %}
       {% if !layoutType() || 'crowi' === layoutType() %}crowi{% else %}growi{% endif %}"
   data-me="{{ user._id.toString() }}"
+  data-is-admin="{{ user.admin }}"
   data-plugin-enabled="{{ isEnabledPlugins() }}"
   {% block html_base_attr %}{% endblock %}
   data-csrftoken="{{ csrf() }}"

+ 0 - 23
lib/views/modal/select_grant_group.html

@@ -1,23 +0,0 @@
-<div class="modal select-grant-group" id="select-grant-group">
-  <div class="modal-dialog">
-    <div class="modal-content">
-
-      <div class="modal-header bg-primary">
-        <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
-        <div class="modal-title">{{ t('SelectGrantGroup') }}</div>
-      </div>
-
-      <div class="modal-body">
-        <p>グループを下のリストから選択</p>
-
-        <ul class="list-inline">
-          {% for sGroup in userRelatedGroups %}
-          <li><button class="btn btn-xs btn-primary" onclick="$('#grant-group').val('{{sGroup.id}}')" data-dismiss="modal">{{sGroup.name}}</button></li>
-          {% endfor %}
-        </ul>
-
-      </div><!-- /.modal-body -->
-
-    </div><!-- /.modal-content -->
-  </div><!-- /.modal-dialog -->
-</div><!-- /.modal -->

+ 0 - 1
lib/views/widget/page_modals.html

@@ -3,4 +3,3 @@
 {% include '../modal/duplicate.html' %}
 {% include '../modal/put_back.html' %}
 {% include '../modal/page_name_warning.html' %}
-{% include '../modal/select_grant_group.html' %}

+ 21 - 26
resource/js/app.js

@@ -13,7 +13,7 @@ import SearchPage       from './components/SearchPage';
 import PageEditor       from './components/PageEditor';
 import OptionsSelector  from './components/PageEditor/OptionsSelector';
 import { EditorOptions, PreviewOptions } from './components/PageEditor/OptionsSelector';
-import GrantSelector, { UserGroup, PageGrant } from './components/PageEditor/GrantSelector';
+import GrantSelector    from './components/PageEditor/GrantSelector';
 import Page             from './components/Page';
 import PageListSearch   from './components/PageListSearch';
 import PageHistory      from './components/PageHistory';
@@ -63,6 +63,7 @@ const isLoggedin = document.querySelector('.main-container.nologin') == null;
 // FIXME
 const crowi = new Crowi({
   me: $('body').data('current-username'),
+  isAdmin: $('body').data('is-admin'),
   csrfToken: $('body').data('csrftoken'),
 }, window);
 window.crowi = crowi;
@@ -187,38 +188,32 @@ if (pageEditorOptionsSelectorElem) {
   );
 }
 // render GrantSelector
-const userRelatedGroupsElem = document.getElementById('user-related-group-data');
 const pageEditorGrantSelectorElem = document.getElementById('page-grant-selector');
-const pageGrantElem = document.getElementById('page-grant');
-const pageGrantGroupElem = document.getElementById('grant-group');
-function updatePageGrantElems(newPageGrant) {
-  pageGrantElem.value = newPageGrant.grant;
-  pageGrantGroupElem.value = newPageGrant.grantGroup.userGroupId || '';
-}
 if (pageEditorGrantSelectorElem) {
-  let userRelatedGroups;
-  if (userRelatedGroupsElem != null) {
-    let userRelatedGroupsJSONString = userRelatedGroupsElem.textContent;
-    if (userRelatedGroupsJSONString != null && userRelatedGroupsJSONString.length > 0) {
-      userRelatedGroups = JSON.parse(userRelatedGroupsJSONString || '{}', (value) => {
-        return new UserGroup(value);
-      });
-    }
+  const grantElem = document.getElementById('page-grant');
+  const grantGroupElem = document.getElementById('grant-group');
+  const grantGroupNameElem = document.getElementById('grant-group-name');
+  /* eslint-disable no-inner-declarations */
+  function updateGrantElem(pageGrant) {
+    grantElem.value = pageGrant;
+  }
+  function updateGrantGroupElem(grantGroupId) {
+    grantGroupElem.value = grantGroupId;
   }
-  pageGrant = new PageGrant();
-  pageGrant.grant = document.getElementById('page-grant').value;
-  const grantGroupData = JSON.parse(document.getElementById('grant-group').textContent || '{}');
-  if (grantGroupData != null) {
-    const grantGroup = new UserGroup();
-    grantGroup.userGroupId = grantGroupData.id;
-    grantGroup.userGroup = grantGroupData;
-    pageGrant.grantGroup = grantGroup;
+  function updateGrantGroupNameElem(grantGroupName) {
+    grantGroupNameElem.value = grantGroupName;
   }
+  /* eslint-enable */
+  const pageGrant = +grantElem.value;
+  const pageGrantGroupId = grantGroupElem.value;
+  const pageGrantGroupName = grantGroupNameElem.value;
   ReactDOM.render(
     <I18nextProvider i18n={i18n}>
       <GrantSelector crowi={crowi}
-        userRelatedGroups={userRelatedGroups} pageGrant={pageGrant}
-        onChange={updatePageGrantElems} />
+        pageGrant={pageGrant} pageGrantGroupId={pageGrantGroupId} pageGrantGroupName={pageGrantGroupName}
+        onChangePageGrant={updateGrantElem}
+        onDeterminePageGrantGroupId={updateGrantGroupElem}
+        onDeterminePageGrantGroupName={updateGrantGroupNameElem} />
     </I18nextProvider>,
     pageEditorGrantSelectorElem
   );

+ 15 - 0
resource/js/components/PageEditor/CodeMirrorEditor.js

@@ -35,6 +35,7 @@ import InterceptorManager from '../../../../lib/util/interceptor-manager';
 
 import PreventMarkdownListInterceptor from './PreventMarkdownListInterceptor';
 import MarkdownTableInterceptor from './MarkdownTableInterceptor';
+import mtu from './MarkdownTableUtil';
 
 export default class CodeMirrorEditor extends AbstractEditor {
 
@@ -46,6 +47,7 @@ export default class CodeMirrorEditor extends AbstractEditor {
       value: this.props.value,
       isEnabledEmojiAutoComplete: false,
       isLoadingKeymap: false,
+      additionalClass: '',
     };
 
     this.init();
@@ -62,6 +64,7 @@ export default class CodeMirrorEditor extends AbstractEditor {
 
     this.scrollCursorIntoViewHandler = this.scrollCursorIntoViewHandler.bind(this);
     this.pasteHandler = this.pasteHandler.bind(this);
+    this.cursorHandler = this.cursorHandler.bind(this);
 
     this.renderLoadingKeymapOverlay = this.renderLoadingKeymapOverlay.bind(this);
   }
@@ -304,6 +307,16 @@ export default class CodeMirrorEditor extends AbstractEditor {
     }
   }
 
+  cursorHandler(editor, event) {
+    const strFromBol = this.getStrFromBol();
+    if (mtu.isEndOfLine(editor) && mtu.linePartOfTableRE.test(strFromBol)) {
+      this.setState({additionalClass: 'autoformat-markdown-table-activated'});
+    }
+    else {
+      this.setState({additionalClass: ''});
+    }
+  }
+
   /**
    * CodeMirror paste event handler
    * see: https://codemirror.net/doc/manual.html#events
@@ -352,6 +365,7 @@ export default class CodeMirrorEditor extends AbstractEditor {
     return <React.Fragment>
       <ReactCodeMirror
         ref="cm"
+        className={this.state.additionalClass}
         editorDidMount={(editor) => {
           // add event handlers
           editor.on('paste', this.pasteHandler);
@@ -385,6 +399,7 @@ export default class CodeMirrorEditor extends AbstractEditor {
             'Ctrl-Q': (cm) => { cm.foldCode(cm.getCursor()) },
           }
         }}
+        onCursor={this.cursorHandler}
         onScroll={(editor, data) => {
           if (this.props.onScroll != null) {
             // add line data

+ 194 - 108
resource/js/components/PageEditor/GrantSelector.js

@@ -4,10 +4,11 @@ import { translate } from 'react-i18next';
 
 import FormGroup from 'react-bootstrap/es/FormGroup';
 import FormControl from 'react-bootstrap/es/FormControl';
-// import ControlLabel from 'react-bootstrap/es/ControlLabel';
-// import Button from 'react-bootstrap/es/Button';
+import ListGroup from 'react-bootstrap/es/ListGroup';
+import ListGroupItem from 'react-bootstrap/es/ListGroupItem';
+import Modal from 'react-bootstrap/es/Modal';
 
-// import Modal from 'react-bootstrap/es/Modal';
+const SPECIFIED_GROUP_VALUE = 'specifiedGroup';
 
 /**
  * Page grant select component
@@ -21,74 +22,129 @@ class GrantSelector extends React.Component {
   constructor(props) {
     super(props);
 
+    this.availableGrants = [
+      { pageGrant: 1, iconClass: 'icon-people', styleClass: '', label: 'Public' },
+      { pageGrant: 2, iconClass: 'icon-link', styleClass: 'text-info', label: 'Anyone with the link' },
+      // { pageGrant: 3, iconClass: '', label: 'Specified users only' },
+      { pageGrant: 4, iconClass: 'icon-lock', styleClass: 'text-danger', label: 'Just me' },
+      { pageGrant: 5, iconClass: 'icon-options', styleClass: '', label: 'Only inside the group' },  // appeared only one of these 'pageGrant: 5'
+      { pageGrant: 5, iconClass: 'icon-options', styleClass: '', label: 'Reselect the group' },     // appeared only one of these 'pageGrant: 5'
+    ];
+
     this.state = {
-      pageGrant: this.props.pageGrant,
-      isGroupModalShown: false,
+      pageGrant: this.props.pageGrant || 1,  // default: 1
+      userRelatedGroups: [],
+      isSelectGroupModalShown: false,
     };
+    if (this.props.pageGrantGroupId !== '') {
+      this.state.pageGrantGroup = {
+        _id: this.props.pageGrantGroupId,
+        name: this.props.pageGrantGroupName
+      };
+    }
 
-    this.availableGrants = [1, 2, /*3, */4, 5];
+    this.showSelectGroupModal = this.showSelectGroupModal.bind(this);
+    this.hideSelectGroupModal = this.hideSelectGroupModal.bind(this);
 
-    this.availableGrantLabels = {
-      1: 'Public',
-      2: 'Anyone with the link',
-      // 3:'Specified users only',
-      4: 'Just me',
-      5: 'Only inside the group',
-    };
+    this.getGroupName = this.getGroupName.bind(this);
 
-    this.onChangeGrant = this.onChangeGrant.bind(this);
+    this.changeGrantHandler = this.changeGrantHandler.bind(this);
+    this.groupListItemClickHandler = this.groupListItemClickHandler.bind(this);
   }
 
-  // Init component when the component did mount.
-  componentDidMount() {
-    this.init();
+  componentDidUpdate(prevProps, prevState) {
+    /*
+     * set SPECIFIED_GROUP_VALUE to grant selector
+     *  cz: bootstrap-select input element has the defferent state to React component
+     */
+    if (this.state.pageGrantGroup != null) {
+      this.grantSelectorInputEl.value = SPECIFIED_GROUP_VALUE;
+    }
+
+    // refresh bootstrap-select
+    // see https://silviomoreto.github.io/bootstrap-select/methods/#selectpickerrefresh
+    $('.page-grant-selector.selectpicker').selectpicker('refresh');
+    //// DIRTY HACK -- 2018.05.25 Yuki Takei
+    // set group name to the bootstrap-select options
+    //  cz: .selectpicker('refresh') doesn't replace data-content
+    $('.page-grant-selector .group-name').text(this.getGroupName());
+
   }
 
-  // Initialize the component.
-  init() {
-    this.grantSelectorInputEl.value = this.state.pageGrant.grant;
+  showSelectGroupModal() {
+    this.retrieveUserGroupRelations();
+    this.setState({ isSelectGroupModalShown: true });
+  }
+  hideSelectGroupModal() {
+    this.setState({ isSelectGroupModalShown: false });
+  }
+
+  getGroupName() {
+    const pageGrantGroup = this.state.pageGrantGroup;
+    return pageGrantGroup ? pageGrantGroup.name : '';
   }
 
   /**
-   * On change event handler for pagegrant.
-   * @param {any} grant page grant
-   * @memberof GrantSelector
+   * Retrieve user-group-relations data from backend
+   */
+  retrieveUserGroupRelations() {
+    this.props.crowi.apiGet('/me/user-group-relations')
+      .then(res => {
+        return res.userGroupRelations;
+      })
+      .then(userGroupRelations => {
+        const userRelatedGroups = userGroupRelations.map(relation => {
+          return relation.relatedGroup;
+        });
+        this.setState({userRelatedGroups});
+      });
+  }
+
+  /**
+   * change event handler for pageGrant selector
    */
-  onChangeGrant(grant) {
-    const newValue = this.grantSelectorInputEl.value;
-    const newGrant = Object.assign(this.state.pageGrant, {grant: newValue});
-    this.setState({ pageGrant: newGrant });
+  changeGrantHandler() {
+    const pageGrant = +this.grantSelectorInputEl.value;
+
+    // select group
+    if (pageGrant === 5) {
+      this.showSelectGroupModal();
+      /*
+       * reset grant selector to state
+       */
+      this.grantSelectorInputEl.value = this.state.pageGrant;
+      return;
+    }
 
+    this.setState({ pageGrant, pageGrantGroup: null });
     // dispatch event
-    this.dispatchOnChange();
+    this.dispatchOnChangePageGrant(pageGrant);
+    this.dispatchOnDeterminePageGrantGroup(null);
   }
 
-  // (TBD)
-  // /**
-  //  * On click event handler for grant usergroup.
-  //  *
-  //  * @memberof GrantSelector
-  //  */
-  // onClickGrantGroup() {
-  //   const newValue = this.groupSelectorInputEl.value;
-  //   const newGrant = Object.assign(this.state.pageGrant, { grantGroup: newValue });
-  //   this.setState({ pageGrant: newGrant });
-
-  //   // dispatch event
-  //   this.dispatchOnChange();
-  //   // close group select modal
-  //   if (this.state.isModalShown) {
-  //     this.setState({ isGroupModalShown: false });
-  //   }
-  // }
+  groupListItemClickHandler(pageGrantGroup) {
+    this.setState({ pageGrant: 5, pageGrantGroup });
 
-  /**
-   * dispatch onChange event
-   * @memberof GrantSelector
-   */
-  dispatchOnChange() {
-    if (this.props.onChange != null) {
-      this.props.onChange(this.state.pageGrant);
+    // dispatch event
+    this.dispatchOnChangePageGrant(5);
+    this.dispatchOnDeterminePageGrantGroup(pageGrantGroup);
+
+    // hide modal
+    this.hideSelectGroupModal();
+  }
+
+  dispatchOnChangePageGrant(pageGrant) {
+    if (this.props.onChangePageGrant != null) {
+      this.props.onChangePageGrant(pageGrant);
+    }
+  }
+
+  dispatchOnDeterminePageGrantGroup(pageGrantGroup) {
+    if (this.props.onDeterminePageGrantGroupId != null) {
+      this.props.onDeterminePageGrantGroupId(pageGrantGroup ? pageGrantGroup._id : '');
+    }
+    if (this.props.onDeterminePageGrantGroupName != null) {
+      this.props.onDeterminePageGrantGroupName(pageGrantGroup ? pageGrantGroup.name : '');
     }
   }
 
@@ -99,16 +155,47 @@ class GrantSelector extends React.Component {
    */
   renderGrantSelector() {
     const { t } = this.props;
+
+    let index = 0;
+    let selectedValue = this.state.pageGrant;
     const grantElems = this.availableGrants.map((grant) => {
-      return <option key={grant} value={grant}>{t(this.availableGrantLabels[grant])}</option>;
+      const dataContent = `<i class="icon icon-fw ${grant.iconClass} ${grant.styleClass}"></i> <span class="${grant.styleClass}">${t(grant.label)}</span>`;
+      return <option key={index++} value={grant.pageGrant} data-content={dataContent}>{t(grant.label)}</option>;
     });
 
-    const bsClassName = 'form-control-dummy'; // set form-control* to shrink width
+    const pageGrantGroup = this.state.pageGrantGroup;
+    if (pageGrantGroup != null) {
+      selectedValue = SPECIFIED_GROUP_VALUE;
+      // DIRTY HACK -- 2018.05.25 Yuki Takei
+      // remove 'Only inside the group' item
+      //  cz: .selectpicker('refresh') doesn't replace data-content
+      grantElems.splice(3, 1);
+    }
+    else {
+      // DIRTY HACK -- 2018.05.25 Yuki Takei
+      // remove 'Reselect the group' item
+      //  cz: .selectpicker('refresh') doesn't replace data-content
+      grantElems.splice(4, 1);
+    }
+
+    /*
+     * react-bootstrap couldn't be rendered only with React feature.
+     * see also 'componentDidUpdate'
+     */
 
+    // add specified group option
+    grantElems.push(
+      <option ref="specifiedGroupOption" key="specifiedGroupKey" value={SPECIFIED_GROUP_VALUE} style={{ display: pageGrantGroup ? 'inherit' : 'none' }}
+          data-content={`<i class="icon icon-fw icon-organization text-success"></i> <span class="group-name text-success">${this.getGroupName()}</span>`}>
+        {this.getGroupName()}
+      </option>
+    );
+
+    const bsClassName = 'form-control-dummy'; // set form-control* to shrink width
     return (
-      <FormGroup controlId="formControlsSelect" className="m-b-0">
-        <FormControl componentClass="select" placeholder="select" defaultValue={this.state.pageGrant.grant} bsClass={bsClassName} className="btn-group-sm selectpicker"
-          onChange={this.onChangeGrant}
+      <FormGroup className="m-b-0">
+        <FormControl componentClass="select" placeholder="select" defaultValue={selectedValue} bsClass={bsClassName} className="btn-group-sm page-grant-selector selectpicker"
+          onChange={this.changeGrantHandler}
           inputRef={ el => this.grantSelectorInputEl=el }>
 
           {grantElems}
@@ -118,57 +205,53 @@ class GrantSelector extends React.Component {
     );
   }
 
-  // (TBD)
-  // /**
-  //  * Render select grantgroup modal.
-  //  *
-  //  * @returns
-  //  * @memberof GrantSelector
-  //  */
-  // renderSelectGroupModal() {
-  //   // const userRelatedGroups = this.props.userRelatedGroups;
-  //   const groupList = this.userRelatedGroups.map((group) => {
-  //     return <li>
-  //         <Button onClick={this.onClickGrantGroup(group)} bsClass="btn btn-sm btn-primary">{group.name}</Button>
-  //       </li>;
-  //   });
-  //   return (
-  //     <Modal show={this.props.isGroupModalShown} className="select-grant-group">
-  //       <Modal.Header closeButton>
-  //         <Modal.Title>
-  //           Select a Group
-  //         </Modal.Title>
-  //       </Modal.Header>
-  //       <Modal.Body>
-
-  //         <ul className="list-inline">
-  //           {groupList}
-  //         </ul>
-  //       </Modal.Body>
-  //     </Modal>
-  //   );
-  // }
-
-  render() {
-    return <div className="m-r-5">{this.renderGrantSelector()}</div>;
-  }
-}
+  /**
+   * Render select grantgroup modal.
+   *
+   * @returns
+   * @memberof GrantSelector
+   */
+  renderSelectGroupModal() {
+    const generateGroupListItems = () => {
+      return this.state.userRelatedGroups.map((group) => {
+        return <ListGroupItem key={group._id} header={group.name} onClick={() => { this.groupListItemClickHandler(group) }}>
+            (TBD) List group members
+          </ListGroupItem>;
+      });
+    };
 
-export class PageGrant {
-  constructor(props) {
-    this.grant = '';
-    this.grantGroup = null;
+    let content = this.state.userRelatedGroups.length === 0
+      ? <div>
+          <h4>There is no group to which you belong.</h4>
+          { this.props.crowi.isAdmin &&
+            <p><a href="/admin/user-groups"><i className="icon icon-fw icon-login"></i> Manage Groups</a></p>
+          }
+        </div>
+      : <ListGroup>
+        {generateGroupListItems()}
+      </ListGroup>;
 
-    Object.assign(this, props);
+    return (
+        <Modal className="select-grant-group"
+          container={this} show={this.state.isSelectGroupModalShown} onHide={this.hideSelectGroupModal}
+        >
+          <Modal.Header closeButton>
+            <Modal.Title>
+              Select a Group
+            </Modal.Title>
+          </Modal.Header>
+          <Modal.Body>
+            {content}
+          </Modal.Body>
+        </Modal>
+    );
   }
-}
 
-export class UserGroup {
-  constructor(props) {
-    this.userGroupId = '';
-    this.userGroup;
-
-    Object.assign(this, props);
+  render() {
+    return <React.Fragment>
+      <div className="m-r-5">{this.renderGrantSelector()}</div>
+      {this.renderSelectGroupModal()}
+    </React.Fragment>;
   }
 }
 
@@ -176,9 +259,12 @@ GrantSelector.propTypes = {
   t: PropTypes.func.isRequired,               // i18next
   crowi: PropTypes.object.isRequired,
   isGroupModalShown: PropTypes.bool,
-  userRelatedGroups: PropTypes.object,
-  pageGrant: PropTypes.instanceOf(PageGrant),
-  onChange: PropTypes.func,
+  pageGrant: PropTypes.number,
+  pageGrantGroupId: PropTypes.string,
+  pageGrantGroupName: PropTypes.string,
+  onChangePageGrant: PropTypes.func,
+  onDeterminePageGrantGroupId: PropTypes.func,
+  onDeterminePageGrantGroupName: PropTypes.func,
 };
 
 export default translate()(GrantSelector);

+ 1 - 1
resource/js/util/Crowi.js

@@ -38,13 +38,13 @@ export default class Crowi {
 
     // FIXME
     this.me = context.me;
+    this.isAdmin = context.isAdmin;
 
     this.users = [];
     this.userByName = {};
     this.userById   = {};
     this.draft = {};
     this.editorOptions = {};
-    this.userRelatedGroups = {};
 
     this.recoverData();
   }

+ 36 - 0
resource/styles/agile-admin/inverse/colors/future.scss

@@ -0,0 +1,36 @@
+@import '../variables';
+
+$basecolor: #16282D;
+$themecolor:rgba(11, 79, 104, 0.616);
+
+$topbar:#011414;
+$sidebar:#fff;
+$bodycolor:$basecolor;
+$headingtext: #D9A364;
+$bodytext: #97D7CF;
+$linktext: darken($themecolor, 15%);
+$linktext-hover: lighten($linktext, 80%);
+$sidebar-text:rgb(65, 133, 124);
+$dark-themecolor:#4F5467;
+
+
+$primary: $themecolor;
+$info: lighten($themecolor,20%);
+
+$logo-mark-fill: rgb(170, 245, 237);
+$wikilinktext: saturate($bodytext, 20%);
+$wikilinktext-hover: darken($wikilinktext, 5%);
+
+$dark: darken($bodytext, 5%);
+$border: lighten($basecolor, 15%);
+$navbar-border: lighten($border, 10%);
+$active-navbar-border: darken($border, 3%);
+$btn-default-bgcolor: darken($basecolor, 10%);
+$inline-code-bg: darken($bodycolor, 5%);
+
+@import 'apply-colors';
+@import 'apply-colors-dark';
+
+.bg-title{
+  border-bottom: 1px solid rgb(131, 228, 215);
+}

+ 8 - 0
resource/styles/scss/_on-edit.scss

@@ -99,6 +99,7 @@ body.on-edit {
         }
       }
 
+
       .page-editor-footer {
         width: 100%;
         margin: 0;
@@ -203,6 +204,13 @@ body.on-edit {
         color: #444;
       }
     }
+    // add icon on cursor
+    .autoformat-markdown-table-activated .CodeMirror-cursor {
+      &:after {
+        font-family: 'FontAwesome';
+        content: '\f0ce';
+      }
+    }
 
     // for Dropzone
     .dropzone {

+ 8 - 0
resource/styles/scss/theme/future.scss

@@ -0,0 +1,8 @@
+// import colors
+@import '../../agile-admin/inverse/colors/future';
+
+// apply agile-admin theme
+@import '../../agile-admin/inverse/style';
+
+// override
+@import 'override-agileadmin';