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

Merge pull request #2216 from weseek/support/reactify-duplicate-page-modal

Support/reactify duplicate page modal
itizawa 5 лет назад
Родитель
Сommit
89a32912dc

+ 3 - 0
src/client/js/app.jsx

@@ -22,6 +22,7 @@ import PageComments from './components/PageComments';
 import PageTimeline from './components/PageTimeline';
 import CommentEditorLazyRenderer from './components/PageComment/CommentEditorLazyRenderer';
 import PageManagement from './components/Page/PageManagement';
+import PageDuplicateModal from './components/PageDuplicateModal';
 import TrashPageAlert from './components/Page/TrashPageAlert';
 import PageAttachment from './components/PageAttachment';
 import PageStatusAlert from './components/PageStatusAlert';
@@ -93,6 +94,8 @@ if (pageContainer.state.pageId != null) {
     'page-attachment': <PageAttachment />,
     'page-comment-write': <CommentEditorLazyRenderer />,
     'page-management': <PageManagement />,
+    'page-duplicate-modal': <PageDuplicateModal />,
+
     'revision-toc': <TableOfContents />,
     'seen-user-list': <UserPictureList userIds={pageContainer.state.seenUserIds} />,
     'liker-list': <UserPictureList userIds={pageContainer.state.likerUserIds} />,

+ 3 - 3
src/client/js/components/Page/PageManagement.jsx

@@ -21,7 +21,7 @@ const PageManagement = (props) => {
         <a className="dropdown-item" href="#" data-target="#renamePage" data-toggle="modal">
           <i className="icon-fw icon-action-redo"></i> { t('Move/Rename') }
         </a>
-        <a className="dropdown-item" href="#" data-target="#duplicatePage" data-toggle="modal">
+        <a className="dropdown-item" type="button" onClick={pageContainer.openPageDuplicateModal}>
           <i className="icon-fw icon-docs"></i> { t('Duplicate') }
         </a>
         <div className="dropdown-divider"></div>
@@ -41,7 +41,7 @@ const PageManagement = (props) => {
   }
 
   return (
-    <li className="nav-item dropdown">
+    <>
       <a
         role="button"
         className={`nav-link dropdown-toggle dropdown-toggle-no-caret ${currentUser == null && 'dropdown-toggle-disabled'}`}
@@ -60,7 +60,7 @@ const PageManagement = (props) => {
         </a>
         {(!isTopPagePath && !isUserPagePath) && renderDropdownItemForDeletablePage()}
       </div>
-    </li>
+    </>
   );
 };
 

+ 107 - 0
src/client/js/components/PageDuplicateModal.jsx

@@ -0,0 +1,107 @@
+import React, { useState } from 'react';
+import PropTypes from 'prop-types';
+
+import {
+  Modal, ModalHeader, ModalBody, ModalFooter,
+} from 'reactstrap';
+
+import { withTranslation } from 'react-i18next';
+
+import { createSubscribedElement } from './UnstatedUtils';
+
+import AppContainer from '../services/AppContainer';
+import PageContainer from '../services/PageContainer';
+import PagePathAutoComplete from './PagePathAutoComplete';
+import ApiErrorMessage from './PageManagement/ApiErrorMessage';
+
+const PageDuplicateModal = (props) => {
+  const { t, appContainer, pageContainer } = props;
+
+  const config = appContainer.getConfig();
+  const isReachable = config.isSearchServiceReachable;
+  const { pageId, path } = pageContainer.state;
+  const { crowi } = appContainer.config;
+
+  const [pageNameInput, setPageNameInput] = useState(path);
+  const [errorCode, setErrorCode] = useState(null);
+  const [errorMessage, setErrorMessage] = useState(null);
+
+  /**
+   * change pageNameInput
+   * @param {string} value
+   */
+  function onChangePageNameInputHandler(value) {
+    setPageNameInput(value);
+  }
+
+  async function clickDuplicateButtonHandler() {
+    try {
+      setErrorCode(null);
+      setErrorMessage(null);
+      const res = await appContainer.apiPost('/pages.duplicate', { page_id: pageId, new_path: pageNameInput });
+      const { page } = res;
+      window.location.href = encodeURI(`${page.path}?duplicated=${path}`);
+    }
+    catch (err) {
+      setErrorCode(err.code);
+      setErrorMessage(err.message);
+    }
+  }
+
+
+  return (
+    <Modal isOpen={pageContainer.state.isPageDuplicateModalShown} toggle={pageContainer.closePageDuplicateModal} className="grw-duplicate-page">
+      <ModalHeader tag="h4" toggle={pageContainer.closePageDuplicateModal} className="bg-primary text-light">
+        { t('modal_duplicate.label.Duplicate page') }
+      </ModalHeader>
+      <ModalBody>
+        <div className="form-group">
+          <label htmlFor="">{ t('modal_duplicate.label.Current page name') }</label><br />
+          <code>{ path }</code>
+        </div>
+        <div className="form-group">
+          <label htmlFor="duplicatePageName">{ t('modal_duplicate.label.New page name') }</label><br />
+          <div className="input-group">
+            <div className="input-group-prepend">
+              <span className="input-group-text">{crowi.url}</span>
+            </div>
+            {isReachable
+              // GW-2355 refactor typeahead
+              ? <PagePathAutoComplete crowi={appContainer} initializedPath={path} addTrailingSlash />
+              : (
+                <input
+                  type="text"
+                  value={pageNameInput}
+                  className="form-control"
+                  onChange={e => onChangePageNameInputHandler(e.target.value)}
+                  required
+                />
+              )}
+          </div>
+        </div>
+      </ModalBody>
+      <ModalFooter>
+        <ApiErrorMessage errorCode={errorCode} errorMessage={errorMessage} linkPath={path} />
+        <button type="button" className="btn btn-primary" onClick={clickDuplicateButtonHandler}>Duplicate page</button>
+      </ModalFooter>
+    </Modal>
+
+  );
+};
+
+
+/**
+ * Wrapper component for using unstated
+ */
+const PageDuplicateModallWrapper = (props) => {
+  return createSubscribedElement(PageDuplicateModal, props, [AppContainer, PageContainer]);
+};
+
+
+PageDuplicateModal.propTypes = {
+  t: PropTypes.func.isRequired, //  i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
+};
+
+export default withTranslation()(PageDuplicateModallWrapper);

+ 76 - 0
src/client/js/components/PageManagement/ApiErrorMessage.jsx

@@ -0,0 +1,76 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import { withTranslation } from 'react-i18next';
+
+const ApiErrorMessage = (props) => {
+  const {
+    t, errorCode, errorMessage, linkPath,
+  } = props;
+
+  function renderMessageByErrorCode() {
+    switch (errorCode) {
+      case 'already_exists':
+        return (
+          <>
+            <strong><i className="icon-fw icon-ban"></i>{ t('page_api_error.already_exists') }</strong>
+            <small><a href={linkPath}>{linkPath} <i className="icon-login"></i></a></small>
+          </>
+        );
+      default:
+        return null;
+    }
+  }
+
+  if (errorCode != null) {
+    return (
+      <span className="text-danger">
+        {renderMessageByErrorCode()}
+      </span>
+    );
+  }
+
+  if (errorMessage != null) {
+    return (
+      <span className="text-danger">
+        {errorMessage}
+      </span>
+    );
+  }
+
+  // render null if no error has occurred
+  return null;
+
+  // TODO GW-79 Set according to error message
+  // <div>
+  //   <span className="text-danger msg msg-notfound_or_forbidden">
+  //     <strong><i className="icon-fw icon-ban"></i>{ t('page_api_error.notfound_or_forbidden') }</strong>
+  //   </span>
+  //   <span className="text-danger msg msg-user_not_admin">
+  //     <strong><i className="icon-fw icon-ban"></i>{ t('page_api_error.user_not_admin') }</strong>
+  //   </span>
+
+  //   <span className="text-warning msg msg-outdated">
+  //     <strong><i className="icon-fw icon-bulb"></i> { t('page_api_error.outdated') }</strong>
+  //     {/* <a href="javascript:location.reload();"> */}
+  //     <i className="fa fa-angle-double-right"></i> { t('Load latest') }
+  //     {/* </a> */}
+  //   </span>
+  //   <span className="text-danger msg msg-invalid_path">
+  //     <strong><i className="icon-fw icon-ban"></i> Invalid path</strong>
+  //   </span>
+  //   <span className="text-danger msg msg-unknown">
+  //     <strong><i className="icon-fw icon-ban"></i> Unknown error occured</strong>
+  //   </span>
+  // </div>
+};
+
+ApiErrorMessage.propTypes = {
+  t:            PropTypes.func.isRequired, //  i18next
+
+  errorCode:    PropTypes.string,
+  errorMessage: PropTypes.string,
+  linkPath:     PropTypes.string,
+};
+
+export default withTranslation()(ApiErrorMessage);

+ 0 - 37
src/client/js/legacy/crowi.js

@@ -299,43 +299,6 @@ $(() => {
     return false;
   });
 
-  // duplicate
-  $('#duplicatePage').on('shown.bs.modal', (e) => {
-    $('#duplicatePage #duplicatePageName').focus();
-    $('#duplicatePage .msg').hide();
-  });
-  $('#duplicatePageForm').submit(function(e) {
-    // create name-value map
-    const nameValueMap = {};
-    $(this).serializeArray().forEach((obj) => {
-      nameValueMap[obj.name] = obj.value; // nameValueMap.new_path is duplicated page path
-    });
-    nameValueMap.socketClientId = websocketContainer.getSocketClientId();
-
-    $.ajax({
-      type: 'POST',
-      url: '/_api/pages.duplicate',
-      data: nameValueMap,
-      dataType: 'json',
-    }).done((res) => {
-      // error
-      if (!res.ok) {
-        const linkPath = pathUtils.normalizePath(nameValueMap.new_path);
-        $('#duplicatePage .msg').hide();
-        $(`#duplicatePage .msg-${res.code}`).show();
-        $('#duplicatePage #linkToNewPage').html(`
-          <a href="${linkPath}">${linkPath} <i class="icon-login"></i></a>
-        `);
-      }
-      else {
-        const page = res.page;
-        window.location.href = `${page.path}?duplicated=${pagePath}`;
-      }
-    });
-
-    return false;
-  });
-
   // empty trash
   $('#emptyTrash').on('shown.bs.modal', (e) => {
     $('#emptyTrash .msg').hide();

+ 9 - 0
src/client/js/services/AppContainer.js

@@ -8,6 +8,8 @@ import InterceptorManager from '@commons/service/interceptor-manager';
 import emojiStrategy from '../util/emojione/emoji_strategy_shrinked.json';
 import GrowiRenderer from '../util/GrowiRenderer';
 
+import Apiv1ErrorHandler from '../util/apiv1ErrorHandler';
+
 import {
   DetachCodeBlockInterceptor,
   RestoreCodeBlockInterceptor,
@@ -428,6 +430,13 @@ export default class AppContainer extends Container {
     if (res.data.ok) {
       return res.data;
     }
+
+    // Return error code if code is exist
+    if (res.data.code != null) {
+      const error = new Apiv1ErrorHandler(res.data.error, res.data.code);
+      throw error;
+    }
+
     throw new Error(res.data.error);
   }
 

+ 13 - 0
src/client/js/services/PageContainer.js

@@ -61,6 +61,8 @@ export default class PageContainer extends Container {
       hasDraftOnHackmd: !!mainContent.getAttribute('data-page-has-draft-on-hackmd'),
       isHackmdDraftUpdatingInRealtime: false,
 
+      isPageDuplicateModalShown: false,
+
       isHeaderSticky: false,
       isSubnavCompact: false,
     };
@@ -86,6 +88,9 @@ export default class PageContainer extends Container {
         isSubnavCompact: scrollThresForCompact < currentYOffset,
       });
     });
+
+    this.openPageDuplicateModal = this.openPageDuplicateModal.bind(this);
+    this.closePageDuplicateModal = this.closePageDuplicateModal.bind(this);
   }
 
   /**
@@ -387,4 +392,12 @@ export default class PageContainer extends Container {
 
   }
 
+  openPageDuplicateModal() {
+    this.setState({ isPageDuplicateModalShown: true });
+  }
+
+  closePageDuplicateModal() {
+    this.setState({ isPageDuplicateModalShown: false });
+  }
+
 }

+ 12 - 0
src/client/js/util/apiv1ErrorHandler.js

@@ -0,0 +1,12 @@
+class Apiv1ErrorHandler extends Error {
+
+  constructor(message = '', code = '') {
+    super();
+
+    this.message = message;
+    this.code = code;
+  }
+
+}
+
+module.exports = Apiv1ErrorHandler;

+ 1 - 46
src/server/views/modal/duplicate.html

@@ -1,46 +1 @@
-  <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 bg-primary text-light">
-          <div class="modal-title">{{ t('modal_duplicate.label.Duplicate page') }}</div>
-          <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
-        </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">
-              <div class="input-group-prepend">
-                <span class="input-group-text">{{ baseUrl }}</span>
-              </div>
-                {% if isSearchServiceConfigured() %}
-                <div id="duplicate-page-name-input" class="page-name-input flex-fill"></div>
-                {% else %}
-                <input type="text" class="form-control" name="new_path" id="duplicatePageName" value="{{ page.path }}">
-                {% endif %}
-              </div>
-            </div>
-        </div>
-        <div class="modal-footer">
-          <div class="d-flex justify-content-between">
-            {% include '../widget/modal/page-api-error-messages.html' %}
-            <div>
-              <input type="hidden" name="_csrf" value="{{ csrf() }}">
-              <input type="hidden" name="path" value="{{ page.path }}">
-              <input type="hidden" name="page_id" value="{{ page._id.toString() }}">
-              <input type="hidden" name="revision_id" value="{{ page.revision._id.toString() }}">
-              <button type="submit" class="btn btn-primary">Duplicate page</button>
-            </div>
-          </div>
-        </div>
-
-      </form>
-      </div><!-- /.modal-content -->
-    </div><!-- /.modal-dialog -->
-  </div><!-- /.modal -->
+<div id ="page-duplicate-modal"></div>

+ 1 - 0
src/server/views/widget/modal/page-api-error-messages.html

@@ -1,3 +1,4 @@
+<!-- TODO GW-79 remove after refactoring -->
 <p>
   <span class="text-danger msg msg-notfound_or_forbidden">
     <strong><i class="icon-fw icon-ban"></i>{{ t('page_api_error.notfound_or_forbidden') }}</strong>