Sfoglia il codice sorgente

Merge branch 'support/reactify-pagemanagement-modal-for-merge' into imprv/create-putback-page-modal-to-react

# Conflicts:
#	src/client/js/components/Page/TrashPageAlert.jsx
ryuichi-e 5 anni fa
parent
commit
5b30a1c2fd

+ 2 - 2
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":{
@@ -338,7 +338,7 @@
   "template": {
     "modal_label": {
       "Create/Edit Template Page": "Create/Edit template page",
-      "Create template under": "Create template page under:"
+      "Create template under": "Create template page under this page"
     },
     "option_label": {
       "create/edit": "Create/Edit template page..",

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

@@ -279,7 +279,7 @@
     "delete_recursively": "全ての子ページも削除",
     "delete_completely": "完全削除",
     "delete_completely_restriction": "完全削除をするための権限がありません。",
-    "recursively": "<code>%s</code> 配下のページも削除します",
+    "recursively": "配下のページも削除します",
     "completely": "ゴミ箱を経由せず、完全に削除します"
   },
   "modal_empty":{
@@ -336,7 +336,7 @@
   "template": {
     "modal_label": {
       "Create/Edit Template Page": "テンプレートページの作成/編集",
-      "Create template under": "以下のパスにテンプレートページを作成"
+      "Create template under": "配下にテンプレートページを作成"
     },
     "option_label": {
       "select": "テンプレートタイプを選択してください",

+ 2 - 0
src/client/js/bootstrap.jsx

@@ -13,6 +13,7 @@ import AppContainer from './services/AppContainer';
 import WebsocketContainer from './services/WebsocketContainer';
 import PageCreateButton from './components/Navbar/PageCreateButton';
 import PageCreateModal from './components/PageCreateModal';
+import CreateTemplateModal from './components/CreateTemplateModal';
 
 const logger = loggerFactory('growi:app');
 
@@ -49,6 +50,7 @@ const componentMappings = {
   'create-page-button': <PageCreateButton />,
   'create-page-button-icon': <PageCreateButton isIcon />,
   'page-create-modal': <PageCreateModal />,
+  'create-template-modal': <CreateTemplateModal />,
 
   'grw-sidebar-wrapper': <Sidebar />,
 

+ 87 - 0
src/client/js/components/CreateTemplateModal.jsx

@@ -0,0 +1,87 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import { Modal, ModalHeader, ModalBody } from 'reactstrap';
+
+import { withTranslation } from 'react-i18next';
+import { pathUtils } from 'growi-commons';
+import urljoin from 'url-join';
+import { createSubscribedElement } from './UnstatedUtils';
+
+import PageContainer from '../services/PageContainer';
+
+const CreateTemplateModal = (props) => {
+  const { t, pageContainer } = props;
+
+  const { path, isCreateTemplatePageModalShown } = pageContainer.state;
+  const parentPath = pathUtils.addTrailingSlash(path);
+
+  function generateUrl(label) {
+    return encodeURI(urljoin(parentPath, label, '#edit'));
+  }
+
+  /**
+   * @param {string} target Which hierarchy to create [children, decendants]
+   */
+  function renderTemplateCard(target, label) {
+    return (
+      <div className="card card-select-template">
+        <div className="card-header">{ t(`template.${target}.label`) }</div>
+        <div className="card-body">
+          <p className="text-center"><code>{label}</code></p>
+          <p className="form-text text-muted text-center"><small>{t(`template.${target}.desc`) }</small></p>
+        </div>
+        <div className="card-footer text-center">
+          <a
+            href={generateUrl(label)}
+            className="btn btn-sm btn-primary"
+            id={`template-button-${target}`}
+          >
+            { t('Edit') }
+          </a>
+        </div>
+      </div>
+    );
+  }
+
+  return (
+    <Modal isOpen={isCreateTemplatePageModalShown} toggle={pageContainer.closeCreateTemplatePageModal} className="grw-create-page">
+      <ModalHeader tag="h4" toggle={pageContainer.closeCreateTemplatePageModal} className="bg-primary text-light">
+        {t('template.modal_label.Create/Edit Template Page')}
+      </ModalHeader>
+      <ModalBody>
+        <div className="form-group">
+          <label className="mb-4">
+            <code>{parentPath}</code><br />
+            { t('template.modal_label.Create template under') }
+          </label>
+          <div className="row">
+            <div className="col-md-6">
+              {renderTemplateCard('children', '_template')}
+            </div>
+            <div className="col-md-6">
+              {renderTemplateCard('decendants', '__template')}
+            </div>
+          </div>
+        </div>
+      </ModalBody>
+    </Modal>
+
+  );
+};
+
+
+/**
+ * Wrapper component for using unstated
+ */
+const CreateTemplateModalWrapper = (props) => {
+  return createSubscribedElement(CreateTemplateModal, props, [PageContainer]);
+};
+
+
+CreateTemplateModal.propTypes = {
+  t: PropTypes.func.isRequired, //  i18next
+  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
+};
+
+export default withTranslation()(CreateTemplateModalWrapper);

+ 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);

+ 23 - 7
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>
       </>
@@ -55,11 +65,17 @@ const PageManagement = (props) => {
       </a>
       <div className="dropdown-menu dropdown-menu-right">
         {!isTopPagePath && renderDropdownItemForNotTopPage()}
-        <a className="dropdown-item" href="#" data-target="#create-template" data-toggle="modal">
+        <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}
+      />
     </>
   );
 };

+ 34 - 13
src/client/js/components/Page/TrashPageAlert.jsx

@@ -10,6 +10,7 @@ import PageContainer from '../../services/PageContainer';
 import UserPicture from '../User/UserPicture';
 import PutbackPageModal from '../PutbackPageModal';
 import EmptyTrashModal from '../EmptyTrashModal';
+import PageDeleteModal from '../PageDeleteModal';
 
 
 const TrashPageAlert = (props) => {
@@ -20,24 +21,33 @@ const TrashPageAlert = (props) => {
   const { currentUser } = appContainer;
   const [isEmptyTrashModalShown, setIsEmptyTrashModalShown] = useState(false);
   const [isPutbackPageModalShown, setIsPutbackPageModalShown] = useState(false);
+  const [isPageDeleteModalShown, setIsPageDeleteModalShown] = useState(false);
 
-  function openEmptyTrashModal() {
+  function openEmptyTrashModalHandler() {
     setIsEmptyTrashModalShown(true);
   }
 
-  function closeEmptyTrashModal() {
+  function closeEmptyTrashModalHandler() {
     setIsEmptyTrashModalShown(false);
   }
 
-  function openPutbackPageModal() {
+  function openPutbackPageModalHandler() {
     setIsPutbackPageModalShown(true);
   }
 
-  function closePutbackPageModal() {
+  function closePutbackPageModalHandler() {
     setIsPutbackPageModalShown(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();
@@ -47,9 +57,9 @@ const TrashPageAlert = (props) => {
     }
   }
 
-  async function onClickPutbackBtn() {
+  async function putBackButtonHandler() {
     try {
-      await /* appContainer.apiv3Delete('/pages/empty-trash'); */
+      await appContainer.apiv3Delete('/pages/empty-trash');
       window.location.reload();
     }
     catch (err) {
@@ -57,6 +67,11 @@ const TrashPageAlert = (props) => {
     }
   }
 
+  function emptyButtonHandler() {
+    emptyTrash();
+  }
+
+
   function renderEmptyButton() {
     return (
       <button
@@ -64,7 +79,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>
@@ -77,7 +92,7 @@ const TrashPageAlert = (props) => {
         <button
           type="button"
           className="btn btn-info rounded-pill btn-sm ml-auto mr-2"
-          onClick={openPutbackPageModal}
+          onClick={openPutbackPageModalHandler}
           data-target="#Putback"
           data-toggle="modal"
         >
@@ -87,8 +102,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>
@@ -106,8 +120,15 @@ const TrashPageAlert = (props) => {
         {(currentUser.admin && path === '/trash' && hasChildren) && renderEmptyButton()}
         {(isDeleted && currentUser != null) && renderTrashPageManagementButtons()}
       </div>
-      <PutbackPageModal isOpen={isPutbackPageModalShown} toggle={closePutbackPageModal} onClickSubmit={onClickPutbackBtn} />
-      <EmptyTrashModal isOpen={isEmptyTrashModalShown} toggle={closeEmptyTrashModal} onClickSubmit={onClickDeleteBtn} />
+      <PutbackPageModal isOpen={isPutbackPageModalShown} onClose={closePutbackPageModalHandler} onClickSubmit={putBackButtonHandler} />
+      <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>

+ 1 - 1
src/client/js/components/PutbackPageModal.jsx

@@ -97,7 +97,7 @@ PutBackPageModal.propTypes = {
 
   page: PropTypes.object.isRequired,
   isOpen: PropTypes.bool.isRequired,
-  toggle: PropTypes.func.isRequired,
+  onClose: PropTypes.func.isRequired,
   onClickSubmit: PropTypes.func.isRequired,
   appContainer: PropTypes.instanceOf(PageContainer).isRequired,
   pageContainer: PropTypes.instanceOf(PageContainer).isRequired,

+ 0 - 50
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) => {
@@ -379,25 +348,6 @@ $(() => {
 
     return false;
   });
-  $('#unlink-page-form').submit((e) => {
-    $.ajax({
-      type: 'POST',
-      url: '/_api/pages.unlink',
-      data: $('#unlink-page-form').serialize(),
-      dataType: 'json',
-    })
-      .done((res) => {
-        if (!res.ok) {
-          $('#delete-errors').html(`<i class="fa fa-times-circle"></i> ${res.error}`);
-          $('#delete-errors').addClass('alert-danger');
-        }
-        else {
-          window.location.href = `${res.path}?unlinked=true`;
-        }
-      });
-
-    return false;
-  });
 
   $('#create-portal-button').on('click', (e) => {
     $('a[data-toggle="tab"][href="#edit"]').tab('show');

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

@@ -4,6 +4,7 @@ import loggerFactory from '@alias/logger';
 
 import * as entities from 'entities';
 import * as toastr from 'toastr';
+import { toastError } from '../util/apiNotification';
 
 const logger = loggerFactory('growi:services:PageContainer');
 const scrollThresForSticky = 0;
@@ -48,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')),
@@ -62,6 +64,7 @@ export default class PageContainer extends Container {
       isHackmdDraftUpdatingInRealtime: false,
 
       isPageDuplicateModalShown: false,
+      isCreateTemplatePageModalShown: false,
 
       isHeaderSticky: false,
       isSubnavCompact: false,
@@ -89,8 +92,23 @@ export default class PageContainer extends Container {
       });
     });
 
+    const unlinkPageButton = document.getElementById('unlink-page-button');
+    if (unlinkPageButton != null) {
+      unlinkPageButton.addEventListener('click', async() => {
+        try {
+          const res = await this.appContainer.apiPost('/pages.unlink', { path });
+          window.location.href = encodeURI(`${res.path}?unlinked=true`);
+        }
+        catch (err) {
+          toastError(err);
+        }
+      });
+    }
+
     this.openPageDuplicateModal = this.openPageDuplicateModal.bind(this);
     this.closePageDuplicateModal = this.closePageDuplicateModal.bind(this);
+    this.openCreateTemplatePageModal = this.openCreateTemplatePageModal.bind(this);
+    this.closeCreateTemplatePageModal = this.closeCreateTemplatePageModal.bind(this);
   }
 
   /**
@@ -300,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,
@@ -400,4 +435,12 @@ export default class PageContainer extends Container {
     this.setState({ isPageDuplicateModalShown: false });
   }
 
+  openCreateTemplatePageModal() {
+    this.setState({ isCreateTemplatePageModalShown: true });
+  }
+
+  closeCreateTemplatePageModal() {
+    this.setState({ isCreateTemplatePageModalShown: false });
+  }
+
 }

+ 1 - 50
src/server/views/modal/create_template.html

@@ -1,50 +1 @@
-{% set templateParentPath = parentPath(page.path | preventXss | escape) %}
-
-<div class="modal create-template" id="create-template">
-  <div class="modal-dialog">
-    <div class="modal-content">
-
-      <div class="modal-header bg-primary text-light">
-        <div class="modal-title">{{ t('template.modal_label.Create/Edit Template 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 class="mb-4">{{ t('template.modal_label.Create template under', templateParentPath ) }}</label>
-          <div class="row">
-            <div class="col-md-6">
-              <div class="card card-select-template">
-                <div class="card-header">{{ t('template.children.label') }}</div>
-                <div class="card-body">
-                  <p class="text-center"><code>_template</code></p>
-                  <p class="form-text text-muted text-center"><small>{{ t('template.children.desc') }}</small></p>
-                </div>
-                <div class="card-footer text-center">
-                  <a href="{{templateParentPath}}_template#edit"
-                      class="btn btn-sm btn-primary" id="template-button-children">
-                      {{ t("Edit") }}
-                  </a>
-                </div>
-              </div>
-            </div>
-            <div class="col-md-6">
-              <div class="card card-select-template">
-                <div class="card-header">{{ t('template.decendants.label') }}</div>
-                <div class="card-body">
-                  <p class="text-center"><code>__template</code></p>
-                  <p class="form-text text-muted text-center"><small>{{ t('template.decendants.desc') }}</small></p>
-                </div>
-                <div class="card-footer text-center">
-                  <a href="{{templateParentPath}}__template#edit"
-                      class="btn btn-sm btn-primary" id="template-button-decendants">
-                      {{ t("Edit") }}
-                  </a>
-                </div>
-              </div>
-            </div>
-          </div>
-        </div>
-      </div>
-    </div><!-- /.modal-content -->
-  </div><!-- /.modal-dialog -->
-</div><!-- /.modal -->
+<div id ="create-template-modal"></div>

+ 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 - 6
src/server/views/widget/page_alerts.html

@@ -40,15 +40,10 @@
         {% endif %}
       </span>
       {% if user and not page.isDeleted() %}
-      <form role="form" id="unlink-page-form" onsubmit="return false;">
-        <input type="hidden" name="_csrf" value="{{ csrf() }}">
-        {# TODO: should be removed by GW-2283 #}
-        <input type="hidden" name="path" value="{{ page.path }}">
-        <button type="submit" class="btn btn-outline-secondary btn-sm float-right">
+        <button type="button" id="unlink-page-button" class="btn btn-secondary btn-sm float-right">
           <i class="ti-unlink" aria-hidden="true"></i>
           Unlink
         </button>
-      </form>
       {% endif %}
     </div>
     {% endif %}

+ 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' %}