소스 검색

Merge pull request #2226 from weseek/support/reactify-delete-completely-modal

Support/reactify delete completely modal
Yuki Takei 5 년 전
부모
커밋
1ae71450e0

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

@@ -281,7 +281,7 @@
     "delete_recursively": "Delete child pages recursively.",
     "delete_completely": "Delete completely",
     "delete_completely_restriction": "You don't have the authority to delete pages completely.",
-    "recursively": "Delete children of <code>%s</code> recursively.",
+    "recursively": "Delete pages under this path recursively.",
     "completely": "Delete completely instead of putting it into trash."
   },
   "modal_empty":{

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

@@ -279,7 +279,7 @@
     "delete_recursively": "全ての子ページも削除",
     "delete_completely": "完全削除",
     "delete_completely_restriction": "完全削除をするための権限がありません。",
-    "recursively": "<code>%s</code> 配下のページも削除します",
+    "recursively": "配下のページも削除します",
     "completely": "ゴミ箱を経由せず、完全に削除します"
   },
   "modal_empty":{

+ 6 - 6
src/client/js/components/EmptyTrashModal.jsx

@@ -9,12 +9,12 @@ import { withTranslation } from 'react-i18next';
 
 const EmptyTrashModal = (props) => {
   const {
-    t, isOpen, toggle, onClickSubmit,
+    t, isOpen, onClose, onClickEmptyBtn,
   } = props;
 
   return (
-    <Modal isOpen={isOpen} toggle={toggle} className="grw-create-page">
-      <ModalHeader tag="h4" toggle={toggle} className="bg-danger text-light">
+    <Modal isOpen={isOpen} toggle={onClose} className="grw-create-page">
+      <ModalHeader tag="h4" toggle={onClose} className="bg-danger text-light">
         { t('modal_empty.empty_the_trash')}
       </ModalHeader>
       <ModalBody>
@@ -22,7 +22,7 @@ const EmptyTrashModal = (props) => {
       </ModalBody>
       <ModalFooter>
         {/* TODO add error message */}
-        <button type="button" className="btn btn-danger" onClick={onClickSubmit}>
+        <button type="button" className="btn btn-danger" onClick={onClickEmptyBtn}>
           <i className="icon-trash mr-2" aria-hidden="true"></i>Empty
         </button>
       </ModalFooter>
@@ -35,8 +35,8 @@ EmptyTrashModal.propTypes = {
   t: PropTypes.func.isRequired, //  i18next
 
   isOpen: PropTypes.bool.isRequired,
-  toggle: PropTypes.func.isRequired,
-  onClickSubmit: PropTypes.func.isRequired,
+  onClose: PropTypes.func.isRequired,
+  onClickEmptyBtn: PropTypes.func.isRequired,
 };
 
 export default withTranslation()(EmptyTrashModal);

+ 22 - 6
src/client/js/components/Page/PageManagement.jsx

@@ -1,19 +1,29 @@
-import React from 'react';
+import React, { useState } from 'react';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 
-import { isTopPage, isUserPage } from '@commons/util/path-utils';
+import { isTopPage } from '@commons/util/path-utils';
 import { createSubscribedElement } from '../UnstatedUtils';
 import AppContainer from '../../services/AppContainer';
 import PageContainer from '../../services/PageContainer';
+import PageDeleteModal from '../PageDeleteModal';
 
 
 const PageManagement = (props) => {
   const { t, appContainer, pageContainer } = props;
-  const { path } = pageContainer.state;
+  const { path, isDeletable, isAbleToDeleteCompletely } = pageContainer.state;
   const { currentUser } = appContainer;
   const isTopPagePath = isTopPage(path);
-  const isUserPagePath = isUserPage(path);
+
+  const [isPageDeleteModalShown, setIsPageDeleteModalShown] = useState(false);
+
+  function openPageDeleteModalHandler() {
+    setIsPageDeleteModalShown(true);
+  }
+
+  function closePageDeleteModalHandler() {
+    setIsPageDeleteModalShown(false);
+  }
 
   function renderDropdownItemForNotTopPage() {
     return (
@@ -33,7 +43,7 @@ const PageManagement = (props) => {
     return (
       <>
         <div className="dropdown-divider"></div>
-        <a className="dropdown-item" href="#" data-target="#deletePage" data-toggle="modal">
+        <a className="dropdown-item" type="button" onClick={openPageDeleteModalHandler}>
           <i className="icon-fw icon-fire text-danger"></i> { t('Delete') }
         </a>
       </>
@@ -58,8 +68,14 @@ const PageManagement = (props) => {
         <a className="dropdown-item" onClick={pageContainer.openCreateTemplatePageModal}>
           <i className="icon-fw icon-magic-wand"></i> { t('template.option_label.create/edit') }
         </a>
-        {(!isTopPagePath && !isUserPagePath) && renderDropdownItemForDeletablePage()}
+        {(!isTopPagePath && isDeletable) && renderDropdownItemForDeletablePage()}
       </div>
+      <PageDeleteModal
+        isOpen={isPageDeleteModalShown}
+        onClose={closePageDeleteModalHandler}
+        path={path}
+        isAbleToDeleteCompletely={isAbleToDeleteCompletely}
+      />
     </>
   );
 };

+ 29 - 7
src/client/js/components/Page/TrashPageAlert.jsx

@@ -8,7 +8,9 @@ import { createSubscribedElement } from '../UnstatedUtils';
 import AppContainer from '../../services/AppContainer';
 import PageContainer from '../../services/PageContainer';
 import UserPicture from '../User/UserPicture';
+
 import EmptyTrashModal from '../EmptyTrashModal';
+import PageDeleteModal from '../PageDeleteModal';
 
 
 const TrashPageAlert = (props) => {
@@ -18,16 +20,25 @@ const TrashPageAlert = (props) => {
   } = pageContainer.state;
   const { currentUser } = appContainer;
   const [isEmptyTrashModalShown, setIsEmptyTrashModalShown] = useState(false);
+  const [isPageDeleteModalShown, setIsPageDeleteModalShown] = useState(false);
 
-  function openEmptyTrashModal() {
+  function openEmptyTrashModalHandler() {
     setIsEmptyTrashModalShown(true);
   }
 
-  function closeEmptyTrashModal() {
+  function closeEmptyTrashModalHandler() {
     setIsEmptyTrashModalShown(false);
   }
 
-  async function onClickDeleteBtn() {
+  function openPageDeleteModalHandler() {
+    setIsPageDeleteModalShown(true);
+  }
+
+  function opclosePageDeleteModalHandler() {
+    setIsPageDeleteModalShown(false);
+  }
+
+  async function emptyTrash() {
     try {
       await appContainer.apiv3Delete('/pages/empty-trash');
       window.location.reload();
@@ -37,6 +48,11 @@ const TrashPageAlert = (props) => {
     }
   }
 
+  function emptyButtonHandler() {
+    emptyTrash();
+  }
+
+
   function renderEmptyButton() {
     return (
       <button
@@ -44,7 +60,7 @@ const TrashPageAlert = (props) => {
         type="button"
         className="btn btn-danger rounded-pill btn-sm ml-auto"
         data-target="#emptyTrash"
-        onClick={openEmptyTrashModal}
+        onClick={openEmptyTrashModalHandler}
       >
         <i className="icon-trash" aria-hidden="true"></i>{ t('modal_empty.empty_the_trash') }
       </button>
@@ -66,8 +82,7 @@ const TrashPageAlert = (props) => {
           type="button"
           className="btn btn-danger rounded-pill btn-sm mr-2"
           disabled={!isAbleToDeleteCompletely}
-          data-target="#deletePage"
-          data-toggle="modal"
+          onClick={openPageDeleteModalHandler}
         >
           <i className="icon-fire" aria-hidden="true"></i> { t('Delete Completely') }
         </button>
@@ -85,7 +100,14 @@ const TrashPageAlert = (props) => {
         {(currentUser.admin && path === '/trash' && hasChildren) && renderEmptyButton()}
         {(isDeleted && currentUser != null) && renderTrashPageManagementButtons()}
       </div>
-      <EmptyTrashModal isOpen={isEmptyTrashModalShown} toggle={closeEmptyTrashModal} onClickSubmit={onClickDeleteBtn} />
+      <EmptyTrashModal isOpen={isEmptyTrashModalShown} onClose={closeEmptyTrashModalHandler} onClickEmptyBtn={emptyButtonHandler} />
+      <PageDeleteModal
+        isOpen={isPageDeleteModalShown}
+        onClose={opclosePageDeleteModalHandler}
+        path={path}
+        isDeleteCompletelyModal
+        isAbleToDeleteCompletely={isAbleToDeleteCompletely}
+      />
     </>
   );
 };

+ 157 - 0
src/client/js/components/PageDeleteModal.jsx

@@ -0,0 +1,157 @@
+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 PageContainer from '../services/PageContainer';
+
+import ApiErrorMessage from './PageManagement/ApiErrorMessage';
+
+const deleteIconAndKey = {
+  completely: {
+    color: 'danger',
+    icon: 'fire',
+    translationKey: 'completely',
+  },
+  temporary: {
+    color: 'primary',
+    icon: 'trash',
+    translationKey: 'page',
+  },
+};
+
+const PageDeleteModal = (props) => {
+  const {
+    t, pageContainer, isOpen, onClose, isDeleteCompletelyModal, path, isAbleToDeleteCompletely,
+  } = props;
+  const [isDeleteRecursively, setIsDeleteRecursively] = useState(true);
+  const [isDeleteCompletely, setIsDeleteCompletely] = useState(isDeleteCompletelyModal && isAbleToDeleteCompletely);
+  const deleteMode = isDeleteCompletely ? 'completely' : 'temporary';
+  const [errorCode, setErrorCode] = useState(null);
+  const [errorMessage, setErrorMessage] = useState(null);
+
+  function changeIsDeleteRecursivelyHandler() {
+    setIsDeleteRecursively(!isDeleteRecursively);
+  }
+
+  function changeIsDeleteCompletelyHandler() {
+    if (!isAbleToDeleteCompletely) {
+      return;
+    }
+    setIsDeleteCompletely(!isDeleteCompletely);
+  }
+
+  async function deletePage() {
+    setErrorCode(null);
+    setErrorMessage(null);
+
+    try {
+      const response = await pageContainer.deletePage(isDeleteRecursively, isDeleteCompletely);
+      const trashPagePath = response.page.path;
+      window.location.href = encodeURI(trashPagePath);
+    }
+    catch (err) {
+      setErrorCode(err.code);
+      setErrorMessage(err.message);
+    }
+  }
+
+  async function deleteButtonHandler() {
+    deletePage();
+  }
+
+  function renderDeleteRecursivelyForm() {
+    return (
+      <div className="custom-control custom-checkbox custom-checkbox-warning">
+        <input
+          className="custom-control-input"
+          id="deleteRecursively"
+          type="checkbox"
+          checked={isDeleteRecursively}
+          onChange={changeIsDeleteRecursivelyHandler}
+        />
+        <label className="custom-control-label" htmlFor="deleteRecursively">
+          { t('modal_delete.delete_recursively') }
+          <p className="form-text text-muted mt-0"><code>{path}</code> { t('modal_delete.recursively') }</p>
+        </label>
+      </div>
+    );
+  }
+
+  function renderDeleteCompletelyForm() {
+    return (
+      <div className="custom-control custom-checkbox custom-checkbox-danger">
+        <input
+          className="custom-control-input"
+          name="completely"
+          id="deleteCompletely"
+          type="checkbox"
+          disabled={!isAbleToDeleteCompletely}
+          checked={isDeleteCompletely}
+          onChange={changeIsDeleteCompletelyHandler}
+        />
+        <label className="custom-control-label text-danger" htmlFor="deleteCompletely">
+          { t('modal_delete.delete_completely') }
+          <p className="form-text text-muted mt-0"> { t('modal_delete.completely') }</p>
+        </label>
+        {!isAbleToDeleteCompletely
+    && <p className="alert alert-warning p-2 my-0"><i className="icon-ban icon-fw"></i>{ t('modal_delete.delete_completely_restriction') }</p>}
+      </div>
+    );
+  }
+
+  return (
+    <Modal isOpen={isOpen} toggle={onClose} className="grw-create-page">
+      <ModalHeader tag="h4" toggle={onClose} className={`bg-${deleteIconAndKey[deleteMode].color} text-light`}>
+        <i className={`icon-fw icon-${deleteIconAndKey[deleteMode].icon}`}></i>
+        { t(`modal_delete.delete_${deleteIconAndKey[deleteMode].translationKey}`) }
+      </ModalHeader>
+      <ModalBody>
+        <div className="form-group">
+          <label>{ t('modal_delete.deleting_page') }:</label><br />
+          <code>{ path }</code>
+        </div>
+        {renderDeleteRecursivelyForm()}
+        {!isDeleteCompletelyModal && renderDeleteCompletelyForm()}
+      </ModalBody>
+      <ModalFooter>
+        <ApiErrorMessage errorCode={errorCode} errorMessage={errorMessage} linkPath={path} />
+        <button type="button" className={`m-l-10 btn btn-${deleteIconAndKey[deleteMode].color}`} onClick={deleteButtonHandler}>
+          <i className={`icon-${deleteIconAndKey[deleteMode].icon}`} aria-hidden="true"></i>
+          { t(`modal_delete.delete_${deleteIconAndKey[deleteMode].translationKey}`) }
+        </button>
+      </ModalFooter>
+    </Modal>
+
+  );
+};
+
+/**
+ * Wrapper component for using unstated
+ */
+const PageDeleteModalWrapper = (props) => {
+  return createSubscribedElement(PageDeleteModal, props, [PageContainer]);
+};
+
+PageDeleteModal.propTypes = {
+  t: PropTypes.func.isRequired, //  i18next
+  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
+
+  isOpen: PropTypes.bool.isRequired,
+  onClose: PropTypes.func.isRequired,
+
+  path: PropTypes.string.isRequired,
+  isDeleteCompletelyModal: PropTypes.bool,
+  isAbleToDeleteCompletely: PropTypes.bool,
+};
+
+PageDeleteModal.defaultProps = {
+  isDeleteCompletelyModal: false,
+};
+
+export default withTranslation()(PageDeleteModalWrapper);

+ 4 - 3
src/client/js/components/PageManagement/ApiErrorMessage.jsx

@@ -17,6 +17,10 @@ const ApiErrorMessage = (props) => {
             <small><a href={linkPath}>{linkPath} <i className="icon-login"></i></a></small>
           </>
         );
+      case 'notfound_or_forbidden':
+        return (
+          <strong><i className="icon-fw icon-ban"></i>{ t('page_api_error.notfound_or_forbidden') }</strong>
+        );
       default:
         return null;
     }
@@ -43,9 +47,6 @@ const ApiErrorMessage = (props) => {
 
   // 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>

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

@@ -323,37 +323,6 @@ $(() => {
 
     return false;
   });
-  // delete
-  $('#deletePage').on('shown.bs.modal', (e) => {
-    $('#deletePage .msg').hide();
-  });
-  $('#delete-page-form').submit((e) => {
-    // create name-value map
-    const nameValueMap = {};
-    $('#delete-page-form').serializeArray().forEach((obj) => {
-      nameValueMap[obj.name] = obj.value;
-    });
-    nameValueMap.socketClientId = websocketContainer.getSocketClientId();
-
-    $.ajax({
-      type: 'POST',
-      url: '/_api/pages.remove',
-      data: nameValueMap,
-      dataType: 'json',
-    }).done((res) => {
-      // error
-      if (!res.ok) {
-        $('#deletePage .msg').hide();
-        $(`#deletePage .msg-${res.code}`).show();
-      }
-      else {
-        const page = res.page;
-        window.location.href = page.path;
-      }
-    });
-
-    return false;
-  });
 
   // Put Back
   $('#putBackPage').on('shown.bs.modal', (e) => {

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

@@ -49,6 +49,7 @@ export default class PageContainer extends Container {
       creator: JSON.parse(mainContent.getAttribute('data-page-creator')),
       updatedAt: mainContent.getAttribute('data-page-updated-at'),
       isDeleted:  JSON.parse(mainContent.getAttribute('data-page-is-deleted')),
+      isDeletable:  JSON.parse(mainContent.getAttribute('data-page-is-deletable')),
       isAbleToDeleteCompletely:  JSON.parse(mainContent.getAttribute('data-page-is-able-to-delete-completely')),
       tags: [],
       hasChildren: JSON.parse(mainContent.getAttribute('data-page-has-children')),
@@ -317,6 +318,23 @@ export default class PageContainer extends Container {
     return { page: res.page, tags: res.tags };
   }
 
+  deletePage(isRecursively, isCompletely) {
+    const websocketContainer = this.appContainer.getContainer('WebsocketContainer');
+
+    // control flag
+    const completely = isCompletely ? true : null;
+    const recursively = isRecursively ? true : null;
+
+    return this.appContainer.apiPost('/pages.remove', {
+      recursively,
+      completely,
+      page_id: this.state.pageId,
+      revision_id: this.state.revisionId,
+      socketClientId: websocketContainer.getSocketClientId(),
+    });
+
+  }
+
   showSuccessToastr() {
     toastr.success(undefined, 'Saved successfully', {
       closeButton: true,

+ 0 - 74
src/server/views/modal/delete.html

@@ -1,74 +0,0 @@
-  <div class="modal" id="deletePage">
-    <div class="modal-dialog">
-      <div class="modal-content">
-
-      <form role="form" id="delete-page-form" onsubmit="return false;">
-
-        <div class="modal-header text-light {% if page.isDeleted() %}bg-danger{% else %}bg-primary{% endif %}">
-          <div class="modal-title">
-            {% if page.isDeleted() %}
-            <i class="icon-fw icon-fire"></i> {{ t('modal_delete.delete_completely') }}
-            {% else %}
-            <i class="icon-fw icon-trash"></i> {{ t('modal_delete.delete_page') }}
-            {% endif %}
-          </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_delete.deleting_page') }}:</label><br>
-            <code>{{ page.path }}</code>
-          </div>
-
-          <hr>
-
-          {% if page.grant != 2 %}
-          <div class="custom-control custom-checkbox custom-checkbox-warning">
-            <input class="custom-control-input" name="recursively" id="cbDeleteRecursively" value="1" type="checkbox" checked>
-            <label class="custom-control-label" for="cbDeleteRecursively">
-              {{ t('modal_delete.delete_recursively') }}
-              <p class="form-text text-muted mt-0"> {{ t('modal_delete.recursively', page.path) }}</p>
-            </label>
-          </div>
-          {% endif %}
-          {% if not page.isDeleted() %}
-          <div class="custom-control custom-checkbox custom-checkbox-danger">
-            <input class="custom-control-input" name="completely" id="cbDeleteCompletely" {% if !user.canDeleteCompletely(page.creator._id) %} disabled="disabled" {% endif %} value="1"  type="checkbox">
-            <label class="custom-control-label" for="cbDeleteCompletely" class="text-danger">
-              {{ t('modal_delete.delete_completely') }}
-              <p class="form-text text-muted mt-0"> {{ t('modal_delete.completely') }}</p>
-            </label>
-            {% if !user.canDeleteCompletely(page.creator._id) %}
-              <p class="alert alert-warning p-2 my-0"><i class="icon-ban icon-fw" ></i>{{ t('modal_delete.delete_completely_restriction') }}</p>
-            {% endif %}
-          </div>
-          {% endif %}
-        </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() }}">
-              {% if page.isDeleted() %}
-                <input type="hidden" name="completely" value="true">
-                <button type="submit" class="m-l-10 btn btn-danger delete-button">
-                  <i class="icon-fire" aria-hidden="true"></i>
-                  {{ t('modal_delete.delete_completely') }}
-                </button>
-              {% else %}
-                <button type="submit" class="m-l-10 btn btn-primary delete-button">
-                  <i class="icon-trash" aria-hidden="true"></i>
-                  {{ t('Delete') }}
-                </button>
-              {% endif %}
-            </div>
-          </div>
-        </div><!-- /.modal-footer -->
-
-      </form>
-      </div><!-- /.modal-content -->
-    </div><!-- /.modal-dialog -->
-  </div><!-- /.modal -->

+ 1 - 0
src/server/views/widget/page_content.html

@@ -12,6 +12,7 @@
   data-page-is-liked="{% if page.isLiked(user) %}true{% else %}false{% endif %}"
   data-page-is-seen="{% if page and page.isSeenUser(user) %}1{% else %}0{% endif %}"
   data-page-is-deleted="{% if page.isDeleted() %}true{% else %}false{% endif %}"
+  data-page-is-deletable="{% if isDeletablePage() %}true{% else %}false{% endif %}"
   data-page-is-able-to-delete-completely="{% if user.canDeleteCompletely(page.creator._id) %}true{% else %}false{% endif %}"
   data-slack-channels="{{ slack|default('') }}"
   data-page-created-at="{% if page %}{{ page.createdAt|datetz('Y/m/d H:i:s') }}{% endif %}"

+ 0 - 1
src/server/views/widget/page_modals.html

@@ -1,6 +1,5 @@
 {% include '../modal/rename.html' %}
 {% include '../modal/empty_trash.html' %}
-{% include '../modal/delete.html' %}
 {% include '../modal/create_template.html' %}
 {% include '../modal/duplicate.html' %}
 {% include '../modal/put_back.html' %}