Преглед изворни кода

Merge pull request #195 from weseek/master

release v2.2.1
Yuki Takei пре 8 година
родитељ
комит
1356bb977d

+ 1 - 0
.gitignore

@@ -17,6 +17,7 @@ npm-debug.log
 /npm-debug.log.*
 /npm-debug.log.*
 
 
 # Dist #
 # Dist #
+/report/
 /public/
 /public/
 /src/*/__build__/
 /src/*/__build__/
 /__build__/**
 /__build__/**

+ 10 - 2
CHANGES.md

@@ -1,6 +1,14 @@
 CHANGES
 CHANGES
 ========
 ========
 
 
+## 2.2.1
+
+* Feature: Duplicate page
+* Improve: Ensure that admin users can remove users waiting for approval
+* Fix: Modal doesn't work with React v16
+* Support: Upgrade React to 16
+* Support: Upgrade outdated libs
+
 ## 2.2.0
 ## 2.2.0
 
 
 * Support: Merge official Crowi v1.6.3
 * Support: Merge official Crowi v1.6.3
@@ -18,8 +26,8 @@ CHANGES
 
 
 ## 2.1.0
 ## 2.1.0
 
 
-* Feat: Adopt Passport the authentication middleware
-* Feat: Selective batch deletion in search result page
+* Feature: Adopt Passport the authentication middleware
+* Feature: Selective batch deletion in search result page
 * Improvement: Ensure to be able to login with both of username or email
 * Improvement: Ensure to be able to login with both of username or email
 * Fix: The problem that couldn't update user data in /me
 * Fix: The problem that couldn't update user data in /me
 * Support: Upgrade outdated libs
 * Support: Upgrade outdated libs

+ 6 - 0
config/webpack.dev.js

@@ -14,10 +14,12 @@ const commonConfig = require('./webpack.common.js');
  */
  */
 const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');
 const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');
 const DllBundlesPlugin = require('webpack-dll-bundles-plugin').DllBundlesPlugin;
 const DllBundlesPlugin = require('webpack-dll-bundles-plugin').DllBundlesPlugin;
+const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
 
 
 /*
 /*
  * Webpack Constants
  * Webpack Constants
  */
  */
+const ANALYZE = process.env.ANALYZE;
 const ENV = process.env.ENV = process.env.NODE_ENV = 'development';
 const ENV = process.env.ENV = process.env.NODE_ENV = 'development';
 const HOST = process.env.HOST || '0.0.0.0';
 const HOST = process.env.HOST || '0.0.0.0';
 const PORT = process.env.PORT || 3000;
 const PORT = process.env.PORT || 3000;
@@ -65,6 +67,10 @@ module.exports = function (options) {
 
 
       new webpack.NoEmitOnErrorsPlugin(),
       new webpack.NoEmitOnErrorsPlugin(),
 
 
+      new BundleAnalyzerPlugin({
+        analyzerMode: ANALYZE ? 'server' : 'disabled',
+      }),
+
     ]
     ]
   });
   });
 }
 }

+ 8 - 0
config/webpack.prod.js

@@ -13,10 +13,12 @@ const commonConfig = require('./webpack.common.js'); // the settings that are co
 const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');
 const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');
 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;
 
 
 /**
 /**
  * Webpack Constants
  * Webpack Constants
  */
  */
+const ANALYZE = process.env.ANALYZE;
 const ENV = process.env.NODE_ENV = process.env.ENV = 'production';
 const ENV = process.env.NODE_ENV = process.env.ENV = 'production';
 const HOST = process.env.HOST || 'localhost';
 const HOST = process.env.HOST || 'localhost';
 const PORT = process.env.PORT || 3000;
 const PORT = process.env.PORT || 3000;
@@ -85,6 +87,12 @@ module.exports = function (env) {
         },
         },
       }),
       }),
 
 
+      new BundleAnalyzerPlugin({
+        analyzerMode: ANALYZE ? 'static' : 'disabled',
+        reportFilename: helpers.root('report/bundle-analyzer.html'),
+        openAnalyzer: false,
+      }),
+
     ],
     ],
 
 
   });
   });

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

@@ -2,6 +2,7 @@
   "Help": "Help",
   "Help": "Help",
   "Edit": "Edit",
   "Edit": "Edit",
   "Delete": "Delete",
   "Delete": "Delete",
+  "Duplicate": "Duplicate",
   "Move": "Move",
   "Move": "Move",
   "Moved": "Moved",
   "Moved": "Moved",
   "Unlinked": "Unlinked",
   "Unlinked": "Unlinked",
@@ -172,6 +173,14 @@
     }
     }
   },
   },
 
 
+  "modal_duplicate": {
+    "label": {
+      "Duplicate page": "Duplicate page",
+      "New page name": "New page name",
+      "Current page name": "Current page name"
+    }
+  },
+
   "modal_putBack": {
   "modal_putBack": {
     "label": {
     "label": {
       "Put Back Page": "Put Back Page",
       "Put Back Page": "Put Back Page",

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

@@ -2,6 +2,7 @@
   "Help": "ヘルプ",
   "Help": "ヘルプ",
   "Edit": "編集",
   "Edit": "編集",
   "Delete": "削除",
   "Delete": "削除",
+  "Duplicate": "複製",
   "Move": "移動",
   "Move": "移動",
   "Moved": "移動しました",
   "Moved": "移動しました",
   "Unlinked": "リダイレクト削除",
   "Unlinked": "リダイレクト削除",
@@ -171,6 +172,14 @@
     }
     }
   },
   },
 
 
+  "modal_duplicate": {
+    "label": {
+      "Duplicate page": "ページを複製する",
+      "New page name": "複製後のページ名",
+      "Current page name": "現在のページ名"
+    }
+  },
+
   "modal_putBack": {
   "modal_putBack": {
     "label": {
     "label": {
       "Put Back Page": "ページを元に戻す",
       "Put Back Page": "ページを元に戻す",

+ 2 - 2
lib/models/user.js

@@ -522,9 +522,9 @@ module.exports = function(crowi) {
       }
       }
 
 
       debug('Removing user:', userData);
       debug('Removing user:', userData);
-      // 物理削除可能なのは、招待中ユーザーのみ
+      // 物理削除可能なのは、承認待ちユーザー、招待中ユーザーのみ
       // 利用を一度開始したユーザーは論理削除のみ可能
       // 利用を一度開始したユーザーは論理削除のみ可能
-      if (userData.status !== STATUS_INVITED) {
+      if (userData.status !== STATUS_REGISTERED && userData.status !== STATUS_INVITED) {
         return callback(new Error('Cannot remove completely the user whoes status is not INVITED'), null);
         return callback(new Error('Cannot remove completely the user whoes status is not INVITED'), null);
       }
       }
 
 

+ 1 - 0
lib/routes/index.js

@@ -138,6 +138,7 @@ module.exports = function(crowi, app) {
   app.post('/_api/pages.remove'       , loginRequired(crowi, app) , csrf, page.api.remove); // (Avoid from API Token)
   app.post('/_api/pages.remove'       , loginRequired(crowi, app) , csrf, page.api.remove); // (Avoid from API Token)
   app.post('/_api/pages.revertRemove' , loginRequired(crowi, app) , csrf, page.api.revertRemove); // (Avoid from API Token)
   app.post('/_api/pages.revertRemove' , loginRequired(crowi, app) , csrf, page.api.revertRemove); // (Avoid from API Token)
   app.post('/_api/pages.unlink'       , loginRequired(crowi, app) , csrf, page.api.unlink); // (Avoid from API Token)
   app.post('/_api/pages.unlink'       , loginRequired(crowi, app) , csrf, page.api.unlink); // (Avoid from API Token)
+  app.post('/_api/pages.duplicate'    , accessTokenParser, loginRequired(crowi, app), csrf, page.api.duplicate);
   app.get('/_api/comments.get'        , accessTokenParser , loginRequired(crowi, app, false) , comment.api.get);
   app.get('/_api/comments.get'        , accessTokenParser , loginRequired(crowi, app, false) , comment.api.get);
   app.post('/_api/comments.add'       , form.comment, accessTokenParser , loginRequired(crowi, app) , csrf, comment.api.add);
   app.post('/_api/comments.add'       , form.comment, accessTokenParser , loginRequired(crowi, app) , csrf, comment.api.add);
   app.post('/_api/comments.remove'    , accessTokenParser , loginRequired(crowi, app) , csrf, comment.api.remove);
   app.post('/_api/comments.remove'    , accessTokenParser , loginRequired(crowi, app) , csrf, comment.api.remove);

+ 23 - 0
lib/routes/page.js

@@ -1142,6 +1142,29 @@ module.exports = function(crowi, app) {
     });
     });
   };
   };
 
 
+  /**
+   * @api {post} /pages.duplicate Duplicate page
+   * @apiName DuplicatePage
+   * @apiGroup Page
+   *
+   * @apiParam {String} page_id Page Id.
+   * @apiParam {String} new_path
+   */
+  api.duplicate = function (req, res) {
+    var pageId = req.body.page_id;
+    var newPagePath = Page.normalizePath(req.body.new_path);
+    var page = {};
+
+    Page.findPageById(pageId)
+      .then(function (pageData) {
+        req.body.path = newPagePath;
+        req.body.body = pageData.revision.body;
+        req.body.grant = pageData.grant;
+
+        return api.create(req, res);
+      });
+  };
+
   /**
   /**
    * @api {post} /pages.unlink Remove the redirecting page
    * @api {post} /pages.unlink Remove the redirecting page
    * @apiName UnlinkPage
    * @apiName UnlinkPage

+ 31 - 21
lib/views/admin/users.html

@@ -197,14 +197,18 @@
                   </li>
                   </li>
                   <li class="divider"></li>
                   <li class="divider"></li>
                   <li class="dropdown-header">ステータス</li>
                   <li class="dropdown-header">ステータス</li>
-                  <li class="dropdown-button">
+
                   {% if sUser.status == 1 %}
                   {% if sUser.status == 1 %}
-                  <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>
+                  <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>
+                  </li>
                   {% endif  %}
                   {% endif  %}
+
                   {% if sUser.status == 2 %}
                   {% if sUser.status == 2 %}
+                  <li class="dropdown-button">
                     {% if sUser.username != user.username %}
                     {% if sUser.username != user.username %}
                     <form action="/admin/user/{{ sUser._id.toString() }}/suspend" method="post">
                     <form action="/admin/user/{{ sUser._id.toString() }}/suspend" method="post">
                       <input type="hidden" name="_csrf" value="{{ csrf() }}">
                       <input type="hidden" name="_csrf" value="{{ csrf() }}">
@@ -215,28 +219,34 @@
                     <br>
                     <br>
                     <p class="alert alert-danger">自分自身のアカウントを停止することはできません</p>
                     <p class="alert alert-danger">自分自身のアカウントを停止することはできません</p>
                     {% endif %}
                     {% endif %}
+                  </li>
                   {% endif %}
                   {% endif %}
+
                   {% if sUser.status == 3 %}
                   {% if sUser.status == 3 %}
-                  <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>
+                  <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>
                   </li>
                   </li>
                   <li class="dropdown-button">
                   <li class="dropdown-button">
-                  {# 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>
-                  {% endif  %}
-                  {% if sUser.status == 5 %}
-                  {# 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>
+                    {# 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>
+                  </li>
                   {% endif  %}
                   {% endif  %}
+
+                  {% if sUser.status == 1 || sUser.status == 5 %}
+                  <li class="dropdown-button">
+                    {# label は同じだけど、こっちは物理削除 #}
+                    <form action="/admin/user/{{ sUser._id.toString() }}/removeCompletely" method="post">
+                      <input type="hidden" name="_csrf" value="{{ csrf() }}">
+                      <button type="submit" class="btn btn-block btn-danger">削除する</button>
+                    </form>
                   </li>
                   </li>
+                  {% endif  %}
 
 
                   {% if sUser.status == 2 %} {# activated な人だけこのメニューを表示 #}
                   {% if sUser.status == 2 %} {# activated な人だけこのメニューを表示 #}
                   <li class="divider"></li>
                   <li class="divider"></li>

+ 36 - 0
lib/views/modal/duplicate.html

@@ -0,0 +1,36 @@
+  <div class="modal" id="duplicatePage">
+    <div class="modal-dialog">
+      <div class="modal-content">
+
+      <form role="form" id="duplicatePageForm" onsubmit="return false;">
+
+        <div class="modal-header">
+          <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>
+        <div class="modal-body">
+            <div class="form-group">
+              <label for="">{{ t('modal_duplicate.label.Current page name') }}</label><br>
+              <code>{{ page.path }}</code>
+            </div>
+            <div class="form-group">
+              <label for="duplicatePageName">{{ t('modal_duplicate.label.New page name') }}</label><br>
+              <div class="input-group">
+                <span class="input-group-addon">{{ config.crowi['app:url'] }}</span>
+                <input type="text" class="form-control" name="new_path" id="duplicatePageName" value="{{ page.path }}">
+              </div>
+            </div>
+        </div>
+        <div class="modal-footer">
+          <p><small class="pull-left" id="duplicatePageNameCheck"></small></p>
+          <input type="hidden" name="_csrf" value="{{ csrf() }}">
+          <input type="hidden" name="path" value="{{ page.path }}">
+          <input type="hidden" name="page_id" value="{{ page._id.toString() }}">
+          <input type="hidden" name="revision_id" value="{{ page.revision._id.toString() }}">
+          <input type="submit" class="btn btn-primary" value="Duplicate page">
+        </div>
+
+      </form>
+      </div><!-- /.modal-content -->
+    </div><!-- /.modal-dialog -->
+  </div><!-- /.modal -->

+ 3 - 0
lib/views/page.html

@@ -120,6 +120,8 @@
       </a>
       </a>
       <ul class="dropdown-menu">
       <ul class="dropdown-menu">
        <li><a href="#" data-target="#renamePage" data-toggle="modal"><i class="fa fa-share"></i> {{ t('Move') }}</a></li>
        <li><a href="#" data-target="#renamePage" data-toggle="modal"><i class="fa fa-share"></i> {{ t('Move') }}</a></li>
+       <li class="divider"></li>
+       <li><a href="#" data-target="#duplicatePage" data-toggle="modal"><i class="fa fa-clone"></i> {{ t('Duplicate') }}</a></li>
        {% if isDeletablePage() %}
        {% if isDeletablePage() %}
        <li class="divider"></li>
        <li class="divider"></li>
        <li class=""><a href="#" data-target="#deletePage" data-toggle="modal"><i class="fa fa-trash-o text-danger"></i> {{ t('Delete') }}</a></li>
        <li class=""><a href="#" data-target="#deletePage" data-toggle="modal"><i class="fa fa-trash-o text-danger"></i> {{ t('Delete') }}</a></li>
@@ -265,6 +267,7 @@
 <div id="crowi-modals">
 <div id="crowi-modals">
   {% include 'modal/rename.html' %}
   {% include 'modal/rename.html' %}
   {% include 'modal/delete.html' %}
   {% include 'modal/delete.html' %}
+  {% include 'modal/duplicate.html' %}
   {% include 'modal/put_back.html' %}
   {% include 'modal/put_back.html' %}
   {% include 'modal/page_name_warning.html' %}
   {% include 'modal/page_name_warning.html' %}
 </div>
 </div>

+ 14 - 9
package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "crowi-plus",
   "name": "crowi-plus",
-  "version": "2.2.0-RC",
+  "version": "2.2.1-RC",
   "description": "Enhanced Crowi",
   "description": "Enhanced Crowi",
   "tags": [
   "tags": [
     "wiki",
     "wiki",
@@ -19,13 +19,16 @@
     "url": "https://github.com/weseek/crowi-plus/issues"
     "url": "https://github.com/weseek/crowi-plus/issues"
   },
   },
   "scripts": {
   "scripts": {
+    "build:dev:analyze": "cross-env ANALYZE=1 npm run build:dev",
     "build:dev:watch": "npm run build:dev -- --watch",
     "build:dev:watch": "npm run build:dev -- --watch",
     "build:dev": "npm run clean:js && webpack --config config/webpack.dev.js  --progress --profile",
     "build:dev": "npm run clean:js && webpack --config config/webpack.dev.js  --progress --profile",
+    "build:prod:analyze": "cross-env ANALYZE=1 npm run build:prod",
     "build:prod": "npm run clean && webpack --config config/webpack.prod.js  --progress --profile --bail",
     "build:prod": "npm run clean && webpack --config config/webpack.prod.js  --progress --profile --bail",
     "build": "npm run build:dev",
     "build": "npm run build:dev",
     "clean:js": "rimraf -- public/js",
     "clean:js": "rimraf -- public/js",
     "clean:dll": "rimraf -- public/dll",
     "clean:dll": "rimraf -- public/dll",
-    "clean": "npm run clean:js && npm run clean:dll",
+    "clean:report": "rimraf -- report",
+    "clean": "npm run clean:js && npm run clean:dll && npm run clean:report",
     "heroku-postbuild": "sh bin/heroku/install-plugins.sh && npm run build:prod",
     "heroku-postbuild": "sh bin/heroku/install-plugins.sh && npm run build:prod",
     "mkdirp": "mkdirp",
     "mkdirp": "mkdirp",
     "plugin:def": "node bin/generate-plugin-definitions-source.js",
     "plugin:def": "node bin/generate-plugin-definitions-source.js",
@@ -49,7 +52,7 @@
     "assets-webpack-plugin": "~3.5.1",
     "assets-webpack-plugin": "~3.5.1",
     "async": "^2.3.0",
     "async": "^2.3.0",
     "aws-sdk": "^2.88.0",
     "aws-sdk": "^2.88.0",
-    "axios": "^0.16.2",
+    "axios": "^0.17.0",
     "babel-core": "^6.25.0",
     "babel-core": "^6.25.0",
     "babel-loader": "^7.1.1",
     "babel-loader": "^7.1.1",
     "babel-preset-env": "^1.6.0",
     "babel-preset-env": "^1.6.0",
@@ -61,6 +64,7 @@
     "connect-flash": "~0.1.1",
     "connect-flash": "~0.1.1",
     "connect-redis": "^3.3.0",
     "connect-redis": "^3.3.0",
     "cookie-parser": "^1.4.3",
     "cookie-parser": "^1.4.3",
+    "cross-env": "^5.0.5",
     "crowi-pluginkit": "^1.1.0",
     "crowi-pluginkit": "^1.1.0",
     "csrf": "~3.0.3",
     "csrf": "~3.0.3",
     "css-loader": "^0.28.0",
     "css-loader": "^0.28.0",
@@ -103,11 +107,11 @@
     "passport-local": "^1.0.0",
     "passport-local": "^1.0.0",
     "pino-clf": "^1.0.2",
     "pino-clf": "^1.0.2",
     "plantuml-encoder": "^1.2.4",
     "plantuml-encoder": "^1.2.4",
-    "react": "15.6.1",
-    "react-bootstrap": "^0.31.0",
+    "react": "^16.0.0",
+    "react-bootstrap": "^0.31.3",
     "react-bootstrap-typeahead": "^1.4.2",
     "react-bootstrap-typeahead": "^1.4.2",
     "react-clipboard.js": "^1.1.2",
     "react-clipboard.js": "^1.1.2",
-    "react-dom": "15.6.1",
+    "react-dom": "^16.0.0",
     "redis": "^2.7.1",
     "redis": "^2.7.1",
     "reveal.js": "^3.5.0",
     "reveal.js": "^3.5.0",
     "rimraf": "^2.6.1",
     "rimraf": "^2.6.1",
@@ -119,7 +123,7 @@
     "toastr": "^2.1.2",
     "toastr": "^2.1.2",
     "uglifycss": "^0.0.27",
     "uglifycss": "^0.0.27",
     "webpack": "^3.1.0",
     "webpack": "^3.1.0",
-    "webpack-dll-bundles-plugin": "^1.0.0-beta.5",
+    "webpack-bundle-analyzer": "^2.9.0",
     "webpack-merge": "~4.1.0"
     "webpack-merge": "~4.1.0"
   },
   },
   "devDependencies": {
   "devDependencies": {
@@ -132,12 +136,13 @@
     "morgan": "^1.8.2",
     "morgan": "^1.8.2",
     "node-dev": "^3.1.3",
     "node-dev": "^3.1.3",
     "sinon": "^4.0.0",
     "sinon": "^4.0.0",
-    "sinon-chai": "^2.13.0"
+    "sinon-chai": "^2.13.0",
+    "webpack-dll-bundles-plugin": "^1.0.0-beta.5"
   },
   },
   "engines": {
   "engines": {
     "node": ">=6.11 <7",
     "node": ">=6.11 <7",
     "npm": ">=4.6 <5",
     "npm": ">=4.6 <5",
-    "yarn": "~1.1.0"
+    "yarn": "^1.1.0"
   },
   },
   "config": {
   "config": {
     "blanket": {
     "blanket": {

+ 0 - 2
resource/js/components/HeaderSearchBox/SearchForm.js

@@ -3,8 +3,6 @@ import { FormGroup, Button, InputGroup } from 'react-bootstrap';
 
 
 import { AsyncTypeahead } from 'react-bootstrap-typeahead';
 import { AsyncTypeahead } from 'react-bootstrap-typeahead';
 
 
-import axios from 'axios'
-
 import UserPicture from '../User/UserPicture';
 import UserPicture from '../User/UserPicture';
 import PageListMeta from '../PageList/PageListMeta';
 import PageListMeta from '../PageList/PageListMeta';
 import PagePath from '../PageList/PagePath';
 import PagePath from '../PageList/PagePath';

+ 39 - 0
resource/js/legacy/crowi.js

@@ -311,6 +311,45 @@ $(function() {
     return false;
     return false;
   });
   });
 
 
+  // duplicate
+  $('#duplicatePage').on('shown.bs.modal', function (e) {
+    $('#duplicatePageName').focus();
+  });
+  $('#duplicatePageForm, #unportalize-form').submit(function (e) {
+    // create name-value map
+    let nameValueMap = {};
+    $(this).serializeArray().forEach((obj) => {
+      nameValueMap[obj.name] = obj.value;
+    })
+
+    $.ajax({
+      type: 'POST',
+      url: '/_api/pages.duplicate',
+      data: $(this).serialize(),
+      dataType: 'json'
+    }).done(function (res) {
+      if (!res.ok) {
+        // if already exists
+        $('#duplicatePageNameCheck').html('<i class="fa fa-times-circle"></i> ' + res.error);
+        $('#duplicatePageNameCheck').addClass('alert-danger');
+        $('#linkToNewPage').html(`
+          <i class="fa fa-fw fa-arrow-right"></i><a href="${nameValueMap.new_path}">${nameValueMap.new_path}</a>
+        `);
+      } else {
+        var page = res.page;
+
+        $('#duplicatePageNameCheck').removeClass('alert-danger');
+        $('#duplicatePageNameCheck').html('<img src="/images/loading_s.gif"> Page duplicated! Redirecting to new page location.');
+
+        setTimeout(function () {
+          top.location.href = page.path + '?duplicated=' + pagePath;
+        }, 1000);
+      }
+    });
+
+    return false;
+  });
+
   // delete
   // delete
   $('#delete-page-form').submit(function(e) {
   $('#delete-page-form').submit(function(e) {
     $.ajax({
     $.ajax({

+ 7 - 2
wercker.yml

@@ -58,11 +58,16 @@ build-prod:
       code: yarn list --depth=0
       code: yarn list --depth=0
 
 
     - script:
     - script:
-      name: npm run build:prod
+      name: npm run build:prod:analyze
       code: |
       code: |
-        npm run build:prod
+        npm run build:prod:analyze
 
 
   after-steps:
   after-steps:
+    - script:
+      name: copy report to artifacts
+      code: |
+        cp -r report $WERCKER_REPORT_ARTIFACTS_DIR
+
     - slack-notifier:
     - slack-notifier:
       url: $SLACK_WEBHOOK_URL
       url: $SLACK_WEBHOOK_URL
       channel: ci
       channel: ci

Разлика између датотеке није приказан због своје велике величине
+ 217 - 227
yarn.lock


Неке датотеке нису приказане због велике количине промена