فهرست منبع

Merge pull request #733 from weseek/master

release v3.2.10
Yuki Takei 7 سال پیش
والد
کامیت
5c3aab9939

+ 8 - 1
CHANGES.md

@@ -1,7 +1,14 @@
 CHANGES
 ========
 
-## 3.2.9-RC
+## 3.2.10-RC
+
+* Fix: Pages in trash are available to create
+* Fix: Couldn't create portal page under Crowi Classic Behavior
+* Fix: Table tag in Timeline/SearchResult missed border and BS3 styles
+
+
+## 3.2.9
 
 * Feature: Attachment Storing to MongoDB GridFS
 * Fix: row/col moving of Spreadsheet like GUI (Handsontable) doesn't work

+ 1 - 1
bin/wercker/trigger-growi-docker.sh

@@ -24,7 +24,7 @@ RESPONSE=`curl -X POST \
       }, \
       { \
         "key": "GROWI_REPOS_GIT_COMMIT", \
-        "value": "'$WERCKER_GIT_COMMIT'" \
+        "value": "'$RELEASE_GIT_COMMIT'" \
       } \
     ] \
   }' \

+ 1 - 1
package.json

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

+ 2 - 2
resource/locales/en-US/translation.json

@@ -512,8 +512,8 @@
     "write_java": "You can write Javascript that is applied to whole system.",
     "attach_title_header": "Add h1 section when create new page automatically",
     "attach_title_header_desc": "Add page path to the first line as h1 section when create new page",
-    "show_document_number": "Manage the number of documents to be displayed",
-    "show_document_number_desc": "Set the number of items to display on one page in Recent Created on the home screen"
+    "recent_created_page_num": "Recent Created Paging num",
+    "recent_created_page_num_desc": "The number of pages to show in Recent Created Page List on the user's home"
   },
 
   "user_management": {

+ 2 - 2
resource/locales/ja/translation.json

@@ -527,8 +527,8 @@
     "write_java": "システム全体に適用されるJavaScriptを記述できます。",
     "attach_title_header": "新規ページ作成時の h1 セクション自動挿入",
     "attach_title_header_desc": "新規作成したページの1行目に、ページのパスを h1 セクションとして挿入します。",
-    "show_document_number": "表示ドキュメント数管理",
-    "show_document_number_desc": "ホーム画面の Recent Created で、1ページに表示する件数を設定します。"
+    "recent_created_page_num": "Recent Created ページングサイズ",
+    "recent_created_page_num_desc": "ホーム画面の Recent Created で、1ページに表示する件数を設定します。"
   },
 
   "user_management": {

+ 6 - 6
src/client/js/app.js

@@ -68,7 +68,7 @@ let pageContent = '';
 let markdown = '';
 let slackChannels;
 if (mainContent !== null) {
-  pageId = mainContent.getAttribute('data-page-id');
+  pageId = mainContent.getAttribute('data-page-id') || null;
   pageRevisionId = mainContent.getAttribute('data-page-revision-id');
   pageRevisionCreatedAt = +mainContent.getAttribute('data-page-revision-created');
   pageRevisionIdHackmdSynced = mainContent.getAttribute('data-page-revision-id-hackmd-synced') || null;
@@ -228,11 +228,7 @@ const saveWithSubmitButton = function() {
   options.socketClientId = socketClientId;
 
   let promise = undefined;
-  if (editorMode === 'builtin') {
-    // get markdown
-    promise = Promise.resolve(componentInstances.pageEditor.getMarkdown());
-  }
-  else {
+  if (editorMode === 'hackmd') {
     // get markdown
     promise = componentInstances.pageEditorByHackmd.getMarkdown();
     // use revisionId of PageEditorByHackmd
@@ -240,6 +236,10 @@ const saveWithSubmitButton = function() {
     // set option to sync
     options.isSyncRevisionToHackmd = true;
   }
+  else {
+    // get markdown
+    promise = Promise.resolve(componentInstances.pageEditor.getMarkdown());
+  }
   // create or update
   if (pageId == null) {
     promise = promise.then(markdown => {

+ 87 - 37
src/client/js/components/InstallerForm.js

@@ -1,49 +1,99 @@
 import React from 'react';
 import PropTypes from 'prop-types';
+import i18next from 'i18next';
 import { translate } from 'react-i18next';
 
 class InstallerForm extends React.Component {
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      isValidUserName: true,
+    };
+    this.checkUserName = this.checkUserName.bind(this);
+  }
+
+  checkUserName(event) {
+    const axios = require('axios').create({
+      headers: {
+        'Content-Type': 'application/json',
+        'X-Requested-With': 'XMLHttpRequest'
+      },
+      responseType: 'json'
+    });
+    axios.get('/_api/check_username', {params: {username: event.target.value}})
+      .then((res) => this.setState({ isValidUserName: res.data.valid }));
+  }
+
+  changeLanguage(locale) {
+    i18next.changeLanguage(locale);
+  }
+
   render() {
+    const hasErrorClass = this.state.isValidUserName ? '' : ' has-error';
+    const unavailableUserId = this.state.isValidUserName ? '' : <span><i className="icon-fw icon-ban" />{ this.props.t('installer.unavaliable_user_id') }</span>;
     return (
-      <form role="form" action="/installer/createAdmin" method="post" id="register-form">
-        <div className="input-group" id="input-group-username">
-          <span className="input-group-addon"><i className="icon-user"></i></span>
-          <input type="text" className="form-control" placeholder={ this.props.t('User ID') }
-            name="registerForm[username]" defaultValue={this.props.userName} required />
-        </div>
-        <p className="help-block">
-          <span id="help-block-username"></span>
+      <div className={'login-dialog p-t-10 p-b-10 col-sm-offset-4 col-sm-4' + hasErrorClass}>
+        <p className="alert alert-success">
+          <strong>{ this.props.t('installer.create_initial_account') }</strong><br />
+          <small>{ this.props.t('installer.initial_account_will_be_administrator_automatically') }</small>
         </p>
 
-        <div className="input-group">
-          <span className="input-group-addon"><i className="icon-tag"></i></span>
-          <input type="text" className="form-control" placeholder={ this.props.t('Name') } name="registerForm[name]" defaultValue={ this.props.name } required />
-        </div>
-
-        <div className="input-group">
-          <span className="input-group-addon"><i className="icon-envelope"></i></span>
-          <input type="email" className="form-control" placeholder={ this.props.t('Email') } name="registerForm[email]" defaultValue={ this.props.email } required />
-        </div>
-
-        <div className="input-group">
-          <span className="input-group-addon"><i className="icon-lock"></i></span>
-          <input type="password" className="form-control" placeholder={ this.props.t('Password') } name="registerForm[password]" required />
-        </div>
-
-        <input type="hidden" name="_csrf" value={ this.props.csrf } />
-        <div className="input-group m-t-30 m-b-20 d-flex justify-content-center">
-          <button type="submit" className="fcbtn btn btn-success btn-1b btn-register">
-            <span className="btn-label"><i className="icon-user-follow"></i></span>
-            { this.props.t('Create') }
-          </button>
-        </div>
-
-        <div className="input-group m-t-30 d-flex justify-content-center">
-          <a href="https://growi.org" className="link-growi-org">
-            <span className="growi">GROWI</span>.<span className="org">ORG</span>
-          </a>
-        </div>
-      </form>
+        <form role="form" action="/installer/createAdmin" method="post" id="register-form">
+          <div className={'input-group' + hasErrorClass}>
+            <span className="input-group-addon"><i className="icon-user" /></span>
+            <input type="text" className="form-control" placeholder={ this.props.t('User ID') }
+              name="registerForm[username]" defaultValue={this.props.userName} onBlur={this.checkUserName} required />
+          </div>
+          <p className="help-block">{ unavailableUserId }</p>
+
+          <div className="input-group">
+            <span className="input-group-addon"><i className="icon-tag" /></span>
+            <input type="text" className="form-control" placeholder={ this.props.t('Name') }
+                   name="registerForm[name]" defaultValue={ this.props.name } required />
+          </div>
+
+          <div className="input-group">
+            <span className="input-group-addon"><i className="icon-envelope" /></span>
+            <input type="email" className="form-control" placeholder={ this.props.t('Email') }
+                   name="registerForm[email]" defaultValue={ this.props.email } required />
+          </div>
+
+          <div className="input-group">
+            <span className="input-group-addon"><i className="icon-lock" /></span>
+            <input type="password" className="form-control" placeholder={ this.props.t('Password') }
+                   name="registerForm[password]" required />
+          </div>
+
+          <input type="hidden" name="_csrf" value={ this.props.csrf } />
+
+          <div className="input-group m-t-20 m-b-20 mx-auto">
+            <div className="radio radio-primary radio-inline">
+              <input type="radio" id="radioLangEn" name="registerForm[app:globalLang]" value="en-US"
+                     defaultChecked={ true } onClick={() => this.changeLanguage('en-US')} />
+              <label htmlFor="radioLangEn">{ this.props.t('English') }</label>
+            </div>
+            <div className="radio radio-primary radio-inline">
+              <input type="radio" id="radioLangJa" name="registerForm[app:globalLang]" value="ja"
+                     defaultChecked={ false } onClick={() => this.changeLanguage('ja')} />
+              <label htmlFor="radioLangJa">{ this.props.t('Japanese') }</label>
+            </div>
+          </div>
+
+          <div className="input-group m-t-30 m-b-20 d-flex justify-content-center">
+            <button type="submit" className="fcbtn btn btn-success btn-1b btn-register">
+              <span className="btn-label"><i className="icon-user-follow" /></span>
+              { this.props.t('Create') }
+            </button>
+          </div>
+
+          <div className="input-group m-t-30 d-flex justify-content-center">
+            <a href="https://growi.org" className="link-growi-org">
+              <span className="growi">GROWI</span>.<span className="org">ORG</span>
+            </a>
+          </div>
+        </form>
+      </div>
     );
   }
 }

+ 4 - 1
src/client/js/legacy/crowi.js

@@ -481,6 +481,7 @@ $(function() {
 
   $('#create-portal-button').on('click', function(e) {
     $('body').addClass('on-edit');
+    $('body').addClass('builtin-editor');
 
     const path = $('.content-main').data('path');
     if (path != '/' && $('.content-main').data('page-id') == '') {
@@ -493,8 +494,10 @@ $(function() {
     }
   });
   $('#portal-form-close').on('click', function(e) {
+    $('#edit').removeClass('active');
     $('body').removeClass('on-edit');
-    return false;
+    $('body').removeClass('builtin-editor');
+    location.hash = '#';
   });
 
   /*

+ 3 - 3
src/client/js/util/GrowiRenderer.js

@@ -99,13 +99,13 @@ export default class GrowiRenderer {
           new TableConfigurer(crowi)
         ]);
         break;
-      case 'comment':
+      // case 'comment':
+      //   break;
+      default:
         this.markdownItConfigurers = this.markdownItConfigurers.concat([
           new TableConfigurer(crowi)
         ]);
         break;
-      default:
-        break;
     }
   }
 

+ 9 - 0
src/server/crowi/index.js

@@ -33,6 +33,7 @@ function Crowi(rootdir) {
   this.cacheDir    = path.join(this.tmpDir, 'cache');
 
   this.config = {};
+  this.configManager = null;
   this.searcher = null;
   this.mailer = {};
   this.passportService = null;
@@ -78,6 +79,8 @@ Crowi.prototype.init = function() {
       return self.setupSessionConfig();
     }).then(function() {
       return self.setupAppConfig();
+    }).then(function() {
+      return self.setupConfigManager();
     }).then(function() {
       return self.scanRuntimeVersions();
     }).then(function() {
@@ -205,6 +208,12 @@ Crowi.prototype.setupAppConfig = function() {
   });
 };
 
+Crowi.prototype.setupConfigManager = async function() {
+  const ConfigManager = require('../service/config-manager');
+  this.configManager = new ConfigManager(this.model('Config'));
+  return await this.configManager.loadConfigs();
+};
+
 Crowi.prototype.setupModels = function() {
   var self = this
     ;

+ 2 - 1
src/server/form/register.js

@@ -9,5 +9,6 @@ module.exports = form(
   field('registerForm.email').required(),
   field('registerForm.password').required().is(/^[\x20-\x7F]{6,}$/),
   field('registerForm.googleId'),
-  field('registerForm.googleImage')
+  field('registerForm.googleImage'),
+  field('registerForm[app:globalLang]')
 );

+ 14 - 0
src/server/models/config.js

@@ -149,6 +149,20 @@ module.exports = function(crowi) {
     return config.markdown[key];
   }
 
+  /**
+   * It is deprecated to use this for anything other than ConfigLoader#load.
+   */
+  configSchema.statics.getDefaultCrowiConfigsObject = function() {
+    return getDefaultCrowiConfigs();
+  };
+
+  /**
+   * It is deprecated to use this for anything other than ConfigLoader#load.
+   */
+  configSchema.statics.getDefaultMarkdownConfigsObject = function() {
+    return getDefaultMarkdownConfigs();
+  };
+
   configSchema.statics.getRestrictGuestModeLabels = function() {
     var labels = {};
     labels[SECURITY_RESTRICT_GUEST_MODE_DENY]     = 'security_setting.guest_mode.deny';

+ 9 - 1
src/server/routes/installer.js

@@ -34,13 +34,16 @@ module.exports = function(crowi, app) {
 
   actions.createAdmin = function(req, res) {
     var registerForm = req.body.registerForm || {};
-    var language = req.language || 'en-US';
 
     if (req.form.isValid) {
       var name = registerForm.name;
       var username = registerForm.username;
       var email = registerForm.email;
       var password = registerForm.password;
+      var language = registerForm['app:globalLang'] || (req.language || 'en-US');
+      // for config.globalLang setting.
+      var langForm = {};
+      langForm['app:globalLang'] = language;
 
       User.createUserByEmailAndPassword(name, username, email, password, language, function(err, userData) {
         if (err) {
@@ -69,6 +72,11 @@ module.exports = function(crowi, app) {
           // create initial pages
           createInitialPages(userData, language);
         });
+
+        // save config settings, and update config cache
+        Config.updateNamespaceByArray('crowi', langForm, function(err, config) {
+          Config.updateConfigCache('crowi', config);
+        });
       });
     }
     else {

+ 200 - 0
src/server/service/config-loader.js

@@ -0,0 +1,200 @@
+const debug = require('debug')('growi:service:ConfigLoader');
+
+const TYPES = {
+  NUMBER:  { parse: (v) => parseInt(v) },
+  STRING:  { parse: (v) => v },
+  BOOLEAN: { parse: (v) => /^(true|1)$/i.test(v) }
+};
+
+/**
+ * The following env vars are excluded because these are currently used before the configuration setup.
+ * - MONGO_URI
+ * - NODE_ENV
+ * - PORT
+ * - REDIS_URI
+ * - SESSION_NAME
+ * - PASSWORD_SEED
+ * - SECRET_TOKEN
+ *
+ *  The commented out item has not yet entered the migration work.
+ *  So, parameters of these are under consideration.
+ */
+const ENV_VAR_NAME_TO_CONFIG_INFO = {
+  // ELASTICSEARCH_URI: {
+  //   ns:      ,
+  //   key:     ,
+  //   type:    ,
+  //   default:
+  // },
+  // FILE_UPLOAD: {
+  //   ns:      ,
+  //   key:     ,
+  //   type:    ,
+  //   default:
+  // },
+  // HACKMD_URI: {
+  //   ns:      ,
+  //   key:     ,
+  //   type:    ,
+  //   default:
+  // },
+  // HACKMD_URI_FOR_SERVER: {
+  //   ns:      ,
+  //   key:     ,
+  //   type:    ,
+  //   default:
+  // },
+  // PLANTUML_URI: {
+  //   ns:      ,
+  //   key:     ,
+  //   type:    ,
+  //   default:
+  // },
+  // BLOCKDIAG_URI: {
+  //   ns:      ,
+  //   key:     ,
+  //   type:    ,
+  //   default:
+  // },
+  // OAUTH_GOOGLE_CLIENT_ID: {
+  //   ns:      'crowi',
+  //   key:     'security:passport-google:clientId',
+  //   type:    ,
+  //   default:
+  // },
+  // OAUTH_GOOGLE_CLIENT_SECRET: {
+  //   ns:      'crowi',
+  //   key:     'security:passport-google:clientSecret',
+  //   type:    ,
+  //   default:
+  // },
+  // OAUTH_GOOGLE_CALLBACK_URI: {
+  //   ns:      'crowi',
+  //   key:     'security:passport-google:callbackUrl',
+  //   type:    ,
+  //   default:
+  // },
+  // OAUTH_GITHUB_CLIENT_ID: {
+  //   ns:      'crowi',
+  //   key:     'security:passport-github:clientId',
+  //   type:    ,
+  //   default:
+  // },
+  // OAUTH_GITHUB_CLIENT_SECRET: {
+  //   ns:      'crowi',
+  //   key:     'security:passport-github:clientSecret',
+  //   type:    ,
+  //   default:
+  // },
+  // OAUTH_GITHUB_CALLBACK_URI: {
+  //   ns:      'crowi',
+  //   key:     'security:passport-github:callbackUrl',
+  //   type:    ,
+  //   default:
+  // },
+  // OAUTH_TWITTER_CONSUMER_KEY: {
+  //   ns:      'crowi',
+  //   key:     'security:passport-twitter:consumerKey',
+  //   type:    ,
+  //   default:
+  // },
+  // OAUTH_TWITTER_CONSUMER_SECRET: {
+  //   ns:      'crowi',
+  //   key:     'security:passport-twitter:consumerSecret',
+  //   type:    ,
+  //   default:
+  // },
+  // OAUTH_TWITTER_CALLBACK_URI: {
+  //   ns:      'crowi',
+  //   key:     'security:passport-twitter:callbackUrl',
+  //   type:    ,
+  //   default:
+  // },
+  SAML_ENTRY_POINT: {
+    ns:      'crowi',
+    key:     'security:passport-saml:entryPoint',
+    type:    TYPES.STRING,
+    default: null
+  },
+  SAML_CALLBACK_URI: {
+    ns:      'crowi',
+    key:     'security:passport-saml:callbackUrl',
+    type:    TYPES.STRING,
+    default: null
+  },
+  SAML_ISSUER: {
+    ns:      'crowi',
+    key:     'security:passport-saml:issuer',
+    type:    TYPES.STRING,
+    default: null
+  },
+  SAML_CERT: {
+    ns:      'crowi',
+    key:     'security:passport-saml:cert',
+    type:    TYPES.STRING,
+    default: null
+  }
+};
+
+class ConfigLoader {
+
+  constructor(configModel) {
+    this.configModel = configModel;
+  }
+
+  /**
+   * return a config object
+   */
+  async load() {
+    const configFromDB = await this.loadFromDB();
+    const configFromEnvVars = this.loadFromEnvVars();
+
+    // merge defaults
+    let mergedConfigFromDB = Object.assign({'crowi': this.configModel.getDefaultCrowiConfigsObject()}, configFromDB);
+    mergedConfigFromDB = Object.assign({'markdown': this.configModel.getDefaultMarkdownConfigsObject()}, mergedConfigFromDB);
+
+    return {
+      fromDB: mergedConfigFromDB,
+      fromEnvVars: configFromEnvVars
+    };
+  }
+
+  async loadFromDB() {
+    const config = {};
+    const docs = await this.configModel.find().exec();
+
+    for (const doc of docs) {
+      if (!config[doc.ns]) {
+        config[doc.ns] = {};
+      }
+      config[doc.ns][doc.key] = JSON.parse(doc.value);
+    }
+
+    debug('ConfigLoader#loadFromDB', config);
+
+    return config;
+  }
+
+  loadFromEnvVars() {
+    const config = {};
+    for (const ENV_VAR_NAME of Object.keys(ENV_VAR_NAME_TO_CONFIG_INFO)) {
+      const configInfo = ENV_VAR_NAME_TO_CONFIG_INFO[ENV_VAR_NAME];
+      if (config[configInfo.ns] === undefined) {
+        config[configInfo.ns] = {};
+      }
+
+      if (process.env[ENV_VAR_NAME] === undefined) {
+        config[configInfo.ns][configInfo.key] = configInfo.default;
+      }
+      else {
+        config[configInfo.ns][configInfo.key] = configInfo.type.parse(process.env[ENV_VAR_NAME]);
+      }
+    }
+
+    debug('ConfigLoader#loadFromEnvVars', config);
+
+    return config;
+  }
+}
+
+module.exports = ConfigLoader;

+ 125 - 0
src/server/service/config-manager.js

@@ -0,0 +1,125 @@
+const ConfigLoader = require('../service/config-loader')
+  , debug = require('debug')('growi:service:ConfigManager');
+
+class ConfigManager {
+
+  constructor(configModel) {
+    this.configModel = configModel;
+    this.configLoader = new ConfigLoader(this.configModel);
+    this.configObject = null;
+  }
+
+  /**
+   * load configs from the database and the environment variables
+   */
+  async loadConfigs() {
+    this.configObject = await this.configLoader.load();
+
+    debug('ConfigManager#loadConfigs', this.configObject);
+  }
+
+  /**
+   * get a config specified by namespace & key
+   *
+   * Basically, search a specified config from configs loaded from database at first
+   * and then from configs loaded from env vars.
+   *
+   * In some case, this search method changes.(not yet implemented)
+   */
+  getConfig(namespace, key) {
+    return this.defaultSearch(namespace, key);
+  }
+
+  /**
+   * private api
+   *
+   * Search a specified config from configs loaded from database at first
+   * and then from configs loaded from env vars.
+   *
+   * the followings are the meanings of each special return value.
+   * - null:      a specified config is not set.
+   * - undefined: a specified config does not exist.
+   */
+  defaultSearch(namespace, key) {
+    if (!this.configExistsInDB(namespace, key) && !this.configExistsInEnvVars(namespace, key)) {
+      return undefined;
+    }
+
+    if (this.configExistsInDB(namespace, key) && !this.configExistsInEnvVars(namespace, key) ) {
+      return this.configObject.fromDB[namespace][key];
+    }
+
+    if (!this.configExistsInDB(namespace, key) && this.configExistsInEnvVars(namespace, key) ) {
+      return this.configObject.fromEnvVars[namespace][key];
+    }
+
+    if (this.configExistsInDB(namespace, key) && this.configExistsInEnvVars(namespace, key) ) {
+      if (this.configObject.fromDB[namespace][key] !== null) {
+        return this.configObject.fromDB[namespace][key];
+      }
+      else {
+        return this.configObject.fromEnvVars[namespace][key];
+      }
+    }
+  }
+
+  /**
+   * check whether a specified config exists in configs loaded from the database
+   * @returns {boolean}
+   */
+  configExistsInDB(namespace, key) {
+    if (this.configObject.fromDB[namespace] === undefined) {
+      return false;
+    }
+
+    return this.configObject.fromDB[namespace][key] !== undefined;
+  }
+
+  /**
+   * check whether a specified config exists in configs loaded from the environment variables
+   * @returns {boolean}
+   */
+  configExistsInEnvVars(namespace, key) {
+    if (this.configObject.fromEnvVars[namespace] === undefined) {
+      return false;
+    }
+
+    return this.configObject.fromEnvVars[namespace][key] !== undefined;
+  }
+
+  /**
+   * update configs by a iterable object consisting of several objects with ns, key, value fields
+   *
+   * For example:
+   * ```
+   *  updateConfigs(
+   *   [{
+   *     ns:    'some namespace 1',
+   *     key:   'some key 1',
+   *     value: 'some value 1'
+   *   }, {
+   *     ns:    'some namespace 2',
+   *     key:   'some key 2',
+   *     value: 'some value 2'
+   *   }]
+   *  );
+   * ```
+   */
+  async updateConfigs(configs) {
+    const results = [];
+    for (const config of configs) {
+      results.push(
+        this.configModel.findOneAndUpdate(
+          { ns: config.ns, key: config.key },
+          { ns: config.ns, key: config.key, value: JSON.stringify(config.value) },
+          { upsert: true, }
+        ).exec()
+      );
+    }
+    await Promise.all(results);
+
+    await this.loadConfigs();
+  }
+}
+
+module.exports = ConfigManager;

+ 76 - 76
src/server/views/admin/customize.html

@@ -216,96 +216,96 @@
       </form>
 
       <form action="/_api/admin/customize/features" method="post" class="form-horizontal" id="customfeaturesSettingForm" role="form">
-      <fieldset>
-      <legend>{{ t('customize_page.Function') }}</legend>
-        <p class="well">{{ t("customize_page.function_choose") }}</p>
+        <fieldset>
+        <legend>{{ t('customize_page.Function') }}</legend>
+          <p class="well">{{ t("customize_page.function_choose") }}</p>
 
-        <div class="form-group">
-          <label for="settingForm[customize:isEnabledTimeline]" class="col-xs-3 control-label">{{ t('customize_page.Timeline function') }}</label>
-          <div class="col-xs-9">
-            <div class="btn-group btn-toggle" data-toggle="buttons">
-              <label class="btn btn-default btn-rounded btn-outline {% if settingForm['customize:isEnabledTimeline'] %}active{% endif %}" data-active-class="primary">
-                <input name="settingForm[customize:isEnabledTimeline]" value="true" type="radio"
-                    {% if true === settingForm['customize:isEnabledTimeline'] %}checked{% endif %}> ON
-              </label>
-              <label class="btn btn-default btn-rounded btn-outline {% if !settingForm['customize:isEnabledTimeline'] %}active{% endif %}" data-active-class="default">
-                <input name="settingForm[customize:isEnabledTimeline]" value="false" type="radio"
-                    {% if !settingForm['customize:isEnabledTimeline'] %}checked{% endif %}> OFF
-              </label>
-            </div>
+          <div class="form-group">
+            <label for="settingForm[customize:isEnabledTimeline]" class="col-xs-3 control-label">{{ t('customize_page.Timeline function') }}</label>
+            <div class="col-xs-9">
+              <div class="btn-group btn-toggle" data-toggle="buttons">
+                <label class="btn btn-default btn-rounded btn-outline {% if settingForm['customize:isEnabledTimeline'] %}active{% endif %}" data-active-class="primary">
+                  <input name="settingForm[customize:isEnabledTimeline]" value="true" type="radio"
+                      {% if true === settingForm['customize:isEnabledTimeline'] %}checked{% endif %}> ON
+                </label>
+                <label class="btn btn-default btn-rounded btn-outline {% if !settingForm['customize:isEnabledTimeline'] %}active{% endif %}" data-active-class="default">
+                  <input name="settingForm[customize:isEnabledTimeline]" value="false" type="radio"
+                      {% if !settingForm['customize:isEnabledTimeline'] %}checked{% endif %}> OFF
+                </label>
+              </div>
 
-            <p class="help-block">
-              {{ t("customize_page.subpage_display") }}
-            </p>
-            <p class="help-block">
-              {{ t("customize_page.performance_decrease") }}<br>
-              {{ t("customize_page.list_page_display") }}
-            </p>
+              <p class="help-block">
+                {{ t("customize_page.subpage_display") }}
+              </p>
+              <p class="help-block">
+                {{ t("customize_page.performance_decrease") }}<br>
+                {{ t("customize_page.list_page_display") }}
+              </p>
+            </div>
           </div>
-        </div>
 
-        <div class="form-group">
-          <label for="settingForm[customize:isSavedStatesOfTabChanges]" class="col-xs-3 control-label">{{ t("customize_page.tab_switch") }}</label>
-          <div class="col-xs-9">
-            <div class="btn-group btn-toggle" data-toggle="buttons">
-              <label class="btn btn-default btn-rounded btn-outline {% if settingForm['customize:isSavedStatesOfTabChanges'] %}active{% endif %}" data-active-class="primary">
-                <input name="settingForm[customize:isSavedStatesOfTabChanges]" value="true" type="radio"
-                    {% if true === settingForm['customize:isSavedStatesOfTabChanges'] %}checked{% endif %}> ON
-              </label>
-              <label class="btn btn-default btn-rounded btn-outline {% if !settingForm['customize:isSavedStatesOfTabChanges'] %}active{% endif %}" data-active-class="default">
-                <input name="settingForm[customize:isSavedStatesOfTabChanges]" value="false" type="radio"
-                    {% if !settingForm['customize:isSavedStatesOfTabChanges'] %}checked{% endif %}> OFF
-              </label>
-            </div>
+          <div class="form-group">
+            <label for="settingForm[customize:isSavedStatesOfTabChanges]" class="col-xs-3 control-label">{{ t("customize_page.tab_switch") }}</label>
+            <div class="col-xs-9">
+              <div class="btn-group btn-toggle" data-toggle="buttons">
+                <label class="btn btn-default btn-rounded btn-outline {% if settingForm['customize:isSavedStatesOfTabChanges'] %}active{% endif %}" data-active-class="primary">
+                  <input name="settingForm[customize:isSavedStatesOfTabChanges]" value="true" type="radio"
+                      {% if true === settingForm['customize:isSavedStatesOfTabChanges'] %}checked{% endif %}> ON
+                </label>
+                <label class="btn btn-default btn-rounded btn-outline {% if !settingForm['customize:isSavedStatesOfTabChanges'] %}active{% endif %}" data-active-class="default">
+                  <input name="settingForm[customize:isSavedStatesOfTabChanges]" value="false" type="radio"
+                      {% if !settingForm['customize:isSavedStatesOfTabChanges'] %}checked{% endif %}> OFF
+                </label>
+              </div>
 
-            <p class="help-block">
-              {{ t("customize_page.save_edit") }}<br>
-              {{ t("customize_page.by_invalidating") }}
-            </p>
+              <p class="help-block">
+                {{ t("customize_page.save_edit") }}<br>
+                {{ t("customize_page.by_invalidating") }}
+              </p>
+            </div>
           </div>
-        </div>
 
-        <div class="form-group">
-          <label for="settingForm[customize:isEnabledAttachTitleHeader]" class="col-xs-3 control-label">{{ t("customize_page.attach_title_header") }}</label>
-          <div class="col-xs-9">
-            <div class="btn-group btn-toggle" data-toggle="buttons">
-              <label class="btn btn-default btn-rounded btn-outline {% if settingForm['customize:isEnabledAttachTitleHeader'] %}active{% endif %}" data-active-class="primary">
-                <input name="settingForm[customize:isEnabledAttachTitleHeader]" value="true" type="radio" {% if true===settingForm['customize:isEnabledAttachTitleHeader'] %}checked{% endif %}> ON
-              </label>
-              <label class="btn btn-default btn-rounded btn-outline {% if !settingForm['customize:isEnabledAttachTitleHeader'] %}active{% endif %}" data-active-class="default">
-                <input name="settingForm[customize:isEnabledAttachTitleHeader]" value="false" type="radio" {% if !settingForm['customize:isEnabledAttachTitleHeader'] %}checked{% endif %}> OFF
-              </label>
-            </div>
+          <div class="form-group">
+            <label for="settingForm[customize:isEnabledAttachTitleHeader]" class="col-xs-3 control-label">{{ t("customize_page.attach_title_header") }}</label>
+            <div class="col-xs-9">
+              <div class="btn-group btn-toggle" data-toggle="buttons">
+                <label class="btn btn-default btn-rounded btn-outline {% if settingForm['customize:isEnabledAttachTitleHeader'] %}active{% endif %}" data-active-class="primary">
+                  <input name="settingForm[customize:isEnabledAttachTitleHeader]" value="true" type="radio" {% if true===settingForm['customize:isEnabledAttachTitleHeader'] %}checked{% endif %}> ON
+                </label>
+                <label class="btn btn-default btn-rounded btn-outline {% if !settingForm['customize:isEnabledAttachTitleHeader'] %}active{% endif %}" data-active-class="default">
+                  <input name="settingForm[customize:isEnabledAttachTitleHeader]" value="false" type="radio" {% if !settingForm['customize:isEnabledAttachTitleHeader'] %}checked{% endif %}> OFF
+                </label>
+              </div>
 
-            <p class="help-block">
-              {{ t("customize_page.attach_title_header_desc") }}
-            </p>
+              <p class="help-block">
+                {{ t("customize_page.attach_title_header_desc") }}
+              </p>
+            </div>
           </div>
-        </div>
 
-        <div class="form-group">
-          <label for="settingForm[customize:showRecentCreatedNumber]" class="col-xs-3 control-label">{{ t("customize_page.show_document_number") }}</label>
-          <div class="col-xs-5">
-            <select class="form-control selectpicker" name="settingForm[customize:showRecentCreatedNumber]" value="{{ settingForm['customize:showRecentCreatedNumber'] }}">
-              <option value="10" {% if 10 == settingForm['customize:showRecentCreatedNumber'] %}selected{% endif %}>10</option>
-              <option value="30" {% if 30 == settingForm['customize:showRecentCreatedNumber'] %}selected{% endif %}>30</option>
-              <option value="50" {% if 50 == settingForm['customize:showRecentCreatedNumber'] %}selected{% endif %}>50</option>
-            </select>
-
-            <p class="help-block">
-              {{ t("customize_page.show_document_number_desc") }}
-            </p>
+          <div class="form-group">
+            <label for="settingForm[customize:showRecentCreatedNumber]" class="col-xs-3 control-label">{{ t("customize_page.recent_created_page_num") }}</label>
+            <div class="col-xs-5">
+              <select class="form-control selectpicker" name="settingForm[customize:showRecentCreatedNumber]" value="{{ settingForm['customize:showRecentCreatedNumber'] }}">
+                <option value="10" {% if 10 == settingForm['customize:showRecentCreatedNumber'] %}selected{% endif %}>10</option>
+                <option value="30" {% if 30 == settingForm['customize:showRecentCreatedNumber'] %}selected{% endif %}>30</option>
+                <option value="50" {% if 50 == settingForm['customize:showRecentCreatedNumber'] %}selected{% endif %}>50</option>
+              </select>
+
+              <p class="help-block">
+                {{ t("customize_page.recent_created_page_num_desc") }}
+              </p>
+            </div>
           </div>
-        </div>
 
-        <div class="form-group">
-          <div class="col-xs-offset-3 col-xs-6">
-            <input type="hidden" name="_csrf" value="{{ csrf() }}">
-            <button type="submit" class="btn btn-primary">{{ t('Update') }}</button>
+          <div class="form-group">
+            <div class="col-xs-offset-3 col-xs-6">
+              <input type="hidden" name="_csrf" value="{{ csrf() }}">
+              <button type="submit" class="btn btn-primary">{{ t('Update') }}</button>
+            </div>
           </div>
-        </div>
 
-      </fieldset>
+        </fieldset>
       </form>
 
       <form action="/_api/admin/customize/highlightJsStyle" method="post" class="form-horizontal" id="customhighlightJsStyleSettingForm" role="form">

+ 5 - 33
src/server/views/installer.html

@@ -44,44 +44,16 @@
       </div>
     </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>{{ t('installer.create_initial_account') }}</strong><br>
-        <small>{{ t('installer.initial_account_will_be_administrator_automatically') }}</small>
-      </p>
-
-      <div id='installer-form'
-        data-user-name="{{ req.body.registerForm.username }}"
-        data-name="{{ googleName|default(req.body.registerForm.name) }}"
-        data-email="{{ googleEmail|default(req.body.registerForm.email) }}"
-        data-csrf="{{ csrf() }}">
-      </div>
+    <div id='installer-form'
+      data-user-name="{{ req.body.registerForm.username }}"
+      data-name="{{ googleName|default(req.body.registerForm.name) }}"
+      data-email="{{ googleEmail|default(req.body.registerForm.email) }}"
+      data-csrf="{{ csrf() }}">
     </div>
 
   </div>{# /.row #}
 
 </div>{# /.main #}
 
-<script>
-$(function() {
-  $('#register-form input[name="registerForm[username]"]').change(function(e) {
-    var username = $(this).val();
-    $('#login-dialog').removeClass('has-error');
-    $('#input-group-username').removeClass('has-error');
-    $('#help-block-username').html("");
-
-    $.getJSON('/_api/check_username', {username: username}, function(json) {
-      if (!json.valid) {
-        $('#help-block-username').html(
-          '<i class="icon-fw icon-ban"></i>{{ t("installer.unavaliable_user_id") }}'
-        );
-        $('#login-dialog').addClass('has-error');
-        $('#input-group-username').addClass('has-error');
-      }
-    });
-  });
-});
-</script>
-
 {% endblock %}
 

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

@@ -1,4 +1,4 @@
 <div class="portal-form-button">
-  <button class="btn btn-primary" id="create-portal-button" {% if not user %}disabled{% endif %}>Create Portal</button>
+  <a class="btn btn-primary" id="create-portal-button" href="#edit" data-toggle="tab" {% if not user %}disabled{% endif %}>Create Portal</a>
   <p class="help-block"><a href="#" data-target="#help-portal" data-toggle="modal"><i class="icon-question"></i> What is Portal?</a></p>
 </div>

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

@@ -5,10 +5,11 @@
     </a>
   </li>
 
+  {% if !isTrashPage() and !page.isDeleted() %}
   <li class="nav-main-left-tab">
     <a {% if user %}href="#edit" data-toggle="tab"{% endif %} class="edit-button {% if not user %}edit-button-disabled{% endif %}">
       <i class="icon-note"></i> {{ t('Create') }}
     </a>
   </li>
-
+  {% endif %}
 </ul>

+ 6 - 4
src/server/views/widget/page_alerts.html

@@ -12,14 +12,16 @@
       </p>
     {% endif %}
 
-    {% if page.isDeleted() %}
+    {% if isTrashPage() %}
     <div class="alert alert-warning alert-trash d-flex align-items-center justify-content-between">
       <div>
         <i class="icon-trash" aria-hidden="true"></i>
-        This page is in the trash.<br>
-        Deleted by <img src="{{ page.lastUpdateUser|picture }}" class="picture picture-sm img-circle"> {{ page.lastUpdateUser.name }} at {{ page.updatedAt|datetz('Y-m-d H:i:s') }}
+        This page is in the trash.
+        {% if page.isDeleted() %}
+        <br>Deleted by <img src="{{ page.lastUpdateUser|picture }}" class="picture picture-sm img-circle"> {{ page.lastUpdateUser.name }} at {{ page.updatedAt|datetz('Y-m-d H:i:s') }}
+        {% endif %}
       </div>
-      {% if user %}
+      {% if page.isDeleted() and user %}
       <ul class="list-inline">
         <li>
           <a href="#" class="btn btn-default btn-rounded btn-sm" data-target="#putBackPage" data-toggle="modal"><i class="icon-action-undo" aria-hidden="true"></i> {{ t('Put Back') }}</a>

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

@@ -81,7 +81,7 @@
 <ul class="nav nav-tabs nav-tabs-create-portal hidden-print">
 
   <li class="nav-main-left-tab">
-    <a id="portal-form-close" href="#">
+    <a id="portal-form-close" href="#" data-toggle="tab">
       <i class="icon-action-undo"></i> {{ t('Cancel') }}
     </a>
   </li>

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

@@ -81,7 +81,7 @@
 <ul class="nav nav-tabs customtab nav-tabs-create-portal hidden-print">
 
   <li class="nav-main-left-tab">
-    <a id="portal-form-close" href="#">
+    <a id="portal-form-close" href="#" data-toggle="tab">
       <i class="icon-action-undo"></i> {{ t('Cancel') }}
     </a>
   </li>

+ 4 - 2
wercker.yml

@@ -147,12 +147,12 @@ release: # would be run on release branch
         TMP_RELEASE_BRANCH=tmp/release-$RELEASE_VERSION
         git checkout -B $TMP_RELEASE_BRANCH
         git push -u origin HEAD:$TMP_RELEASE_BRANCH
-        TARGET_COMMITISH=`git rev-parse HEAD`
+        export RELEASE_GIT_COMMIT=`git rev-parse HEAD`
 
     - github-create-release:
       token: $GITHUB_TOKEN
       tag: v$RELEASE_VERSION
-      target-commitish: $TARGET_COMMITISH
+      target-commitish: $RELEASE_GIT_COMMIT
 
     - script:
       name: remove temporary release branch
@@ -180,7 +180,9 @@ release-rc: # would be run on rc/* branches
       name: get RELEASE_VERSION
       code: |
         export RELEASE_VERSION=`npm run version --silent`
+        export RELEASE_GIT_COMMIT=$WERCKER_GIT_COMMIT
         echo "export RELEASE_VERSION=$RELEASE_VERSION"
+        echo "export RELEASE_GIT_COMMIT=$RELEASE_GIT_COMMIT"
 
     - script:
       name: trigger growi-docker release-rc pipeline