Przeglądaj źródła

Merge pull request #2241 from weseek/support/reactify-pagemanagement-modal-for-merge

Support/reactify pagemanagement modal for merge
Yuki Takei 5 lat temu
rodzic
commit
d6986b3322
40 zmienionych plików z 1362 dodań i 929 usunięć
  1. 7 6
      resource/locales/en-US/translation.json
  2. 8 7
      resource/locales/ja/translation.json
  3. 8 3
      src/client/js/app.jsx
  4. 0 2
      src/client/js/bootstrap.jsx
  5. 90 0
      src/client/js/components/CreateTemplateModal.jsx
  6. 73 0
      src/client/js/components/EmptyTrashModal.jsx
  7. 1 1
      src/client/js/components/Me/ExternalAccountRow.jsx
  8. 1 2
      src/client/js/components/Me/PersonalSettings.jsx
  9. 150 0
      src/client/js/components/Page/PageManagement.jsx
  10. 138 0
      src/client/js/components/Page/TrashPageAlert.jsx
  11. 83 51
      src/client/js/components/PageCreateModal.jsx
  12. 161 0
      src/client/js/components/PageDeleteModal.jsx
  13. 130 0
      src/client/js/components/PageDuplicateModal.jsx
  14. 83 0
      src/client/js/components/PageManagement/ApiErrorMessage.jsx
  15. 38 40
      src/client/js/components/PagePathAutoComplete.jsx
  16. 172 0
      src/client/js/components/PageRenameModal.jsx
  17. 104 0
      src/client/js/components/PutbackPageModal.jsx
  18. 0 256
      src/client/js/legacy/crowi.js
  19. 9 0
      src/client/js/services/AppContainer.js
  20. 66 2
      src/client/js/services/PageContainer.js
  21. 12 0
      src/client/js/util/apiv1ErrorHandler.js
  22. 14 60
      src/client/styles/scss/_create-page.scss
  23. 1 1
      src/server/routes/page.js
  24. 0 4
      src/server/views/layout-growi/page.html
  25. 0 3
      src/server/views/layout-growi/page_list.html
  26. 0 4
      src/server/views/layout-kibela/page.html
  27. 0 3
      src/server/views/layout-kibela/page_list.html
  28. 3 12
      src/server/views/layout/layout.html
  29. 0 50
      src/server/views/modal/create_template.html
  30. 0 74
      src/server/views/modal/delete.html
  31. 0 46
      src/server/views/modal/duplicate.html
  32. 0 47
      src/server/views/modal/empty_trash.html
  33. 0 45
      src/server/views/modal/put_back.html
  34. 0 77
      src/server/views/modal/rename.html
  35. 0 24
      src/server/views/widget/modal/page-api-error-messages.html
  36. 2 25
      src/server/views/widget/page_alerts.html
  37. 6 0
      src/server/views/widget/page_content.html
  38. 0 6
      src/server/views/widget/page_modals.html
  39. 1 38
      src/server/views/widget/page_tabs.html
  40. 1 40
      src/server/views/widget/page_tabs_kibela.html

+ 7 - 6
resource/locales/en-US/translation.json

@@ -270,9 +270,9 @@
       "Redirect": "Redirect"
     },
     "help": {
-      "redirect": "Redirect to new page if someone accesses <code>%s</code>",
+      "redirect": "Redirect to new page if someone accesses under this path",
       "metadata": "Remains last update user and updated date as is",
-      "recursive": "Move/Rename children of under <code>%s</code> recursively"
+      "recursive": "Move/Rename children of under this path recursively"
     }
   },
   "Put Back": "Put back",
@@ -283,11 +283,12 @@
     "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":{
-    "empty_the_trash": "Empty The Trash"
+    "empty_the_trash": "Empty The Trash",
+    "notice": "The pages deleted completely are unrecoverable."
   },
   "modal_duplicate": {
     "label": {
@@ -302,7 +303,7 @@
       "recursively": "Put back recursively"
     },
     "help": {
-      "recursively": "Put back children of under <code>%s</code> recursively"
+      "recursively": "Put back page under this path recursively"
     }
   },
   "modal_shortcuts": {
@@ -339,7 +340,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..",

+ 8 - 7
resource/locales/ja/translation.json

@@ -268,9 +268,9 @@
       "Redirect": "リダイレクトする"
     },
     "help": {
-      "redirect": "<code class='text-break'>%s</code> にアクセスされた際に自動的に新しいページにジャンプします",
+      "redirect": "アクセスされた際に自動的に新しいページにジャンプします",
       "metadata": "最終更新ユーザー、最終更新日を更新せず維持します",
-      "recursive": "<code class='text-break'>%s</code> 配下のページも移動/名前変更します"
+      "recursive": "配下のページも移動/名前変更します"
     }
   },
   "Put Back": "元に戻す",
@@ -281,11 +281,12 @@
     "delete_recursively": "全ての子ページも削除",
     "delete_completely": "完全削除",
     "delete_completely_restriction": "完全削除をするための権限がありません。",
-    "recursively": "<code>%s</code> 配下のページも削除します",
+    "recursively": "配下のページも削除します",
     "completely": "ゴミ箱を経由せず、完全に削除します"
   },
   "modal_empty":{
-    "empty_the_trash": "ゴミ箱を空にする"
+    "empty_the_trash": "ゴミ箱を空にする",
+    "notice": "完全削除したページは元に戻すことができません"
   },
   "modal_duplicate": {
     "label": {
@@ -300,7 +301,7 @@
       "recursively": "全ての子ページも元に戻す"
     },
     "help": {
-      "recursively": "<code>%s</code> 配下のページも元に戻します"
+      "recursively": "配下のページも元に戻します"
     }
   },
   "modal_shortcuts": {
@@ -332,12 +333,12 @@
     "activate_user_success": "{{username}}を有効化しました",
     "deactivate_user_success": "{{username}}を無効化しました",
     "remove_user_success": "{{username}}を削除しました",
-    "remove_external_user_success": "{{accountId}}を削除しました "
+    "remove_external_user_success": "{{accountId}}を削除しました"
   },
   "template": {
     "modal_label": {
       "Create/Edit Template Page": "テンプレートページの作成/編集",
-      "Create template under": "以下のパスにテンプレートページを作成"
+      "Create template under": "配下にテンプレートページを作成"
     },
     "option_label": {
       "select": "テンプレートタイプを選択してください",

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

@@ -21,6 +21,8 @@ import PageHistory from './components/PageHistory';
 import PageComments from './components/PageComments';
 import PageTimeline from './components/PageTimeline';
 import CommentEditorLazyRenderer from './components/PageComment/CommentEditorLazyRenderer';
+import PageManagement from './components/Page/PageManagement';
+import TrashPageAlert from './components/Page/TrashPageAlert';
 import PageAttachment from './components/PageAttachment';
 import PageStatusAlert from './components/PageStatusAlert';
 import PagePathAutoComplete from './components/PagePathAutoComplete';
@@ -28,6 +30,7 @@ import RecentCreated from './components/RecentCreated/RecentCreated';
 import MyDraftList from './components/MyDraftList/MyDraftList';
 import UserPictureList from './components/User/UserPictureList';
 import TableOfContents from './components/TableOfContents';
+import PageCreateModal from './components/PageCreateModal';
 
 import PersonalSettings from './components/Me/PersonalSettings';
 import PageContainer from './services/PageContainer';
@@ -68,14 +71,14 @@ Object.assign(componentMappings, {
   // 'revision-history': <PageHistory pageId={pageId} />,
   'tags-page': <TagsList crowi={appContainer} />,
 
-  'create-page-name-input': <PagePathAutoComplete crowi={appContainer} initializedPath={pageContainer.state.path} addTrailingSlash />,
-
   'page-editor': <PageEditor />,
   'page-editor-path-nav': <PagePathNavForEditor />,
   'page-editor-options-selector': <OptionsSelector crowi={appContainer} />,
   'page-status-alert': <PageStatusAlert />,
   'save-page-controls': <SavePageControls />,
 
+  'trash-page-alert': <TrashPageAlert />,
+
   'page-timeline': <PageTimeline />,
 
   'personal-setting': <PersonalSettings crowi={personalContainer} />,
@@ -88,11 +91,13 @@ if (pageContainer.state.pageId != null) {
     'page-comments-list': <PageComments />,
     'page-attachment': <PageAttachment />,
     'page-comment-write': <CommentEditorLazyRenderer />,
+    'page-management': <PageManagement />,
+
     'revision-toc': <TableOfContents />,
     'seen-user-list': <UserPictureList userIds={pageContainer.state.seenUserIds} />,
     'liker-list': <UserPictureList userIds={pageContainer.state.likerUserIds} />,
     'rename-page-name-input': <PagePathAutoComplete crowi={appContainer} initializedPath={pageContainer.state.path} />,
-    'duplicate-page-name-input': <PagePathAutoComplete crowi={appContainer} initializedPath={pageContainer.state.path} />,
+    'page-create-modal': <PageCreateModal />,
 
     'user-created-list': <RecentCreated />,
     'user-draft-list': <MyDraftList />,

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

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

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

@@ -0,0 +1,90 @@
+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 } = 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={props.isOpen} toggle={props.onClose} className="grw-create-page">
+      <ModalHeader tag="h4" toggle={props.onClose} 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,
+
+  isOpen: PropTypes.bool.isRequired,
+  onClose: PropTypes.func.isRequired,
+};
+
+export default withTranslation()(CreateTemplateModalWrapper);

+ 73 - 0
src/client/js/components/EmptyTrashModal.jsx

@@ -0,0 +1,73 @@
+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 ApiErrorMessage from './PageManagement/ApiErrorMessage';
+
+const EmptyTrashModal = (props) => {
+  const {
+    t, isOpen, onClose, appContainer,
+  } = props;
+  const [errorCode, setErrorCode] = useState(null);
+  const [errorMessage, setErrorMessage] = useState(null);
+
+  async function emptyTrash() {
+    setErrorCode(null);
+    setErrorMessage(null);
+    try {
+      await appContainer.apiv3Delete('/pages/empty-trash');
+      window.location.reload();
+    }
+    catch (err) {
+      setErrorCode(err.code);
+      setErrorMessage(err.message);
+    }
+  }
+
+  function emptyButtonHandler() {
+    emptyTrash();
+  }
+
+  return (
+    <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>
+        { t('modal_empty.notice')}
+      </ModalBody>
+      <ModalFooter>
+        <ApiErrorMessage errorCode={errorCode} errorMessage={errorMessage} />
+        <button type="button" className="btn btn-danger" onClick={emptyButtonHandler}>
+          <i className="icon-trash mr-2" aria-hidden="true"></i>Empty
+        </button>
+      </ModalFooter>
+    </Modal>
+  );
+};
+
+/**
+ * Wrapper component for using unstated
+ */
+const EmptyTrashModalWrapper = (props) => {
+  return createSubscribedElement(EmptyTrashModal, props, [AppContainer]);
+};
+
+
+EmptyTrashModal.propTypes = {
+  t: PropTypes.func.isRequired, //  i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+
+  isOpen: PropTypes.bool.isRequired,
+  onClose: PropTypes.func.isRequired,
+  onClickEmptyBtn: PropTypes.func.isRequired,
+};
+
+export default withTranslation()(EmptyTrashModalWrapper);

+ 1 - 1
src/client/js/components/Me/ExternalAccountRow.jsx

@@ -18,7 +18,7 @@ const ExternalAccountRow = (props) => {
       <td className="text-center">
         <button
           type="button"
-          className="btn btn-outline-secondary btn-sm btn-danger"
+          className="btn btn-sm btn-danger"
           onClick={() => props.openDisassociateModal(account)}
         >
           <i className="ti-unlink"></i>

+ 1 - 2
src/client/js/components/Me/PersonalSettings.jsx

@@ -15,7 +15,6 @@ class PersonalSettings extends React.Component {
 
     return (
       <Fragment>
-        {/* TODO GW-226 adapt BS4 */}
         <div className="m-t-10">
           <div className="personal-settings">
             <ul className="nav nav-tabs" role="tablist">
@@ -24,7 +23,7 @@ class PersonalSettings extends React.Component {
               </li>
               <li className="nav-item">
                 <a className="nav-link" href="#external-accounts" data-toggle="tab" role="tab">
-                  <i className="icon-share-alt"></i>
+                  <i className="icon-share-alt mr-1"></i>
                   { t('admin:user_management.external_accounts') }
                 </a>
               </li>

+ 150 - 0
src/client/js/components/Page/PageManagement.jsx

@@ -0,0 +1,150 @@
+import React, { useState } from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+
+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';
+import PageRenameModal from '../PageRenameModal';
+import PageDuplicateModal from '../PageDuplicateModal';
+import CreateTemplateModal from '../CreateTemplateModal';
+
+
+const PageManagement = (props) => {
+  const { t, appContainer, pageContainer } = props;
+  const { path, isDeletable, isAbleToDeleteCompletely } = pageContainer.state;
+
+  const { currentUser } = appContainer;
+  const isTopPagePath = isTopPage(path);
+
+  const [isPageRenameModalShown, setIsPageRenameModalShown] = useState(false);
+  const [isPageDuplicateModalShown, setIsPageDuplicateModalShown] = useState(false);
+  const [isPageTemplateModalShown, setIsPageTempleteModalShown] = useState(false);
+  const [isPageDeleteModalShown, setIsPageDeleteModalShown] = useState(false);
+
+  function openPageRenameModalHandler() {
+    setIsPageRenameModalShown(true);
+  }
+
+  function closePageRenameModalHandler() {
+    setIsPageRenameModalShown(false);
+  }
+
+  function openPageDuplicateModalHandler() {
+    setIsPageDuplicateModalShown(true);
+  }
+
+  function closePageDuplicateModalHandler() {
+    setIsPageDuplicateModalShown(false);
+  }
+
+  function openPageTemplateModalHandler() {
+    setIsPageTempleteModalShown(true);
+  }
+
+  function closePageTemplateModalHandler() {
+    setIsPageTempleteModalShown(false);
+  }
+
+  function openPageDeleteModalHandler() {
+    setIsPageDeleteModalShown(true);
+  }
+
+  function closePageDeleteModalHandler() {
+    setIsPageDeleteModalShown(false);
+  }
+
+
+  function renderDropdownItemForNotTopPage() {
+    return (
+      <>
+        <a className="dropdown-item" type="button" onClick={openPageRenameModalHandler}>
+          <i className="icon-fw icon-action-redo"></i> { t('Move/Rename') }
+        </a>
+        <a className="dropdown-item" type="button" onClick={openPageDuplicateModalHandler}>
+          <i className="icon-fw icon-docs"></i> { t('Duplicate') }
+        </a>
+        <div className="dropdown-divider"></div>
+      </>
+    );
+  }
+
+  function renderDropdownItemForDeletablePage() {
+    return (
+      <>
+        <div className="dropdown-divider"></div>
+        <a className="dropdown-item" type="button" onClick={openPageDeleteModalHandler}>
+          <i className="icon-fw icon-fire text-danger"></i> { t('Delete') }
+        </a>
+      </>
+    );
+  }
+
+  function renderModals() {
+    return (
+      <>
+        <PageRenameModal
+          isOpen={isPageRenameModalShown}
+          onClose={closePageRenameModalHandler}
+          path={path}
+        />
+        <PageDuplicateModal
+          isOpen={isPageDuplicateModalShown}
+          onClose={closePageDuplicateModalHandler}
+        />
+        <CreateTemplateModal
+          isOpen={isPageTemplateModalShown}
+          onClose={closePageTemplateModalHandler}
+        />
+        <PageDeleteModal
+          isOpen={isPageDeleteModalShown}
+          onClose={closePageDeleteModalHandler}
+          path={path}
+          isAbleToDeleteCompletely={isAbleToDeleteCompletely}
+        />
+      </>
+    );
+  }
+
+  return (
+    <>
+      <a
+        role="button"
+        className={`nav-link dropdown-toggle dropdown-toggle-no-caret ${currentUser == null && 'dropdown-toggle-disabled'}`}
+        href="#"
+        data-toggle={`${currentUser == null ? 'tooltip' : 'dropdown'}`}
+        data-placement="top"
+        data-container="body"
+        title={t('Not available for guest')}
+      >
+        <i className="icon-options-vertical"></i>
+      </a>
+      <div className="dropdown-menu dropdown-menu-right">
+        {!isTopPagePath && renderDropdownItemForNotTopPage()}
+        <a className="dropdown-item" type="button" onClick={openPageTemplateModalHandler}>
+          <i className="icon-fw icon-magic-wand"></i> { t('template.option_label.create/edit') }
+        </a>
+        {(!isTopPagePath && isDeletable) && renderDropdownItemForDeletablePage()}
+      </div>
+      {renderModals()}
+    </>
+  );
+};
+
+/**
+ * Wrapper component for using unstated
+ */
+const PageManagementWrapper = (props) => {
+  return createSubscribedElement(PageManagement, props, [AppContainer, PageContainer]);
+};
+
+
+PageManagement.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
+};
+
+export default withTranslation()(PageManagementWrapper);

+ 138 - 0
src/client/js/components/Page/TrashPageAlert.jsx

@@ -0,0 +1,138 @@
+import React, { useState } from 'react';
+import PropTypes from 'prop-types';
+
+import { withTranslation } from 'react-i18next';
+
+import { createSubscribedElement } from '../UnstatedUtils';
+import AppContainer from '../../services/AppContainer';
+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) => {
+  const { t, appContainer, pageContainer } = props;
+  const {
+    path, isDeleted, revisionAuthor, updatedAt, hasChildren, isAbleToDeleteCompletely,
+  } = pageContainer.state;
+  const { currentUser } = appContainer;
+  const [isEmptyTrashModalShown, setIsEmptyTrashModalShown] = useState(false);
+  const [isPutbackPageModalShown, setIsPutbackPageModalShown] = useState(false);
+  const [isPageDeleteModalShown, setIsPageDeleteModalShown] = useState(false);
+
+  function openEmptyTrashModalHandler() {
+    setIsEmptyTrashModalShown(true);
+  }
+
+  function closeEmptyTrashModalHandler() {
+    setIsEmptyTrashModalShown(false);
+  }
+
+  function openPutbackPageModalHandler() {
+    setIsPutbackPageModalShown(true);
+  }
+
+  function closePutbackPageModalHandler() {
+    setIsPutbackPageModalShown(false);
+  }
+
+  function openPageDeleteModalHandler() {
+    setIsPageDeleteModalShown(true);
+  }
+
+  function opclosePageDeleteModalHandler() {
+    setIsPageDeleteModalShown(false);
+  }
+
+  function renderEmptyButton() {
+    return (
+      <button
+        href="#"
+        type="button"
+        className="btn btn-danger rounded-pill btn-sm ml-auto"
+        data-target="#emptyTrash"
+        onClick={openEmptyTrashModalHandler}
+      >
+        <i className="icon-trash" aria-hidden="true"></i>{ t('modal_empty.empty_the_trash') }
+      </button>
+    );
+  }
+
+  function renderTrashPageManagementButtons() {
+    return (
+      <>
+        <button
+          type="button"
+          className="btn btn-info rounded-pill btn-sm ml-auto mr-2"
+          onClick={openPutbackPageModalHandler}
+          data-toggle="modal"
+        >
+          <i className="icon-action-undo" aria-hidden="true"></i> { t('Put Back') }
+        </button>
+        <button
+          type="button"
+          className="btn btn-danger rounded-pill btn-sm mr-2"
+          disabled={!isAbleToDeleteCompletely}
+          onClick={openPageDeleteModalHandler}
+        >
+          <i className="icon-fire" aria-hidden="true"></i> { t('Delete Completely') }
+        </button>
+      </>
+    );
+  }
+
+  function renderModals() {
+    return (
+      <>
+        <EmptyTrashModal
+          isOpen={isEmptyTrashModalShown}
+          onClose={closeEmptyTrashModalHandler}
+        />
+        <PutbackPageModal
+          isOpen={isPutbackPageModalShown}
+          onClose={closePutbackPageModalHandler}
+          path={path}
+        />
+        <PageDeleteModal
+          isOpen={isPageDeleteModalShown}
+          onClose={opclosePageDeleteModalHandler}
+          path={path}
+          isDeleteCompletelyModal
+          isAbleToDeleteCompletely={isAbleToDeleteCompletely}
+        />
+      </>
+    );
+  }
+
+  return (
+    <>
+      <div className="alert alert-warning py-3 px-4 d-flex align-items-center">
+        <div>
+          This page is in the trash <i className="icon-trash" aria-hidden="true"></i>.
+          {isDeleted && <span><br /><UserPicture user={revisionAuthor} /> Deleted by {revisionAuthor.name} at {updatedAt}</span>}
+        </div>
+        {(currentUser.admin && path === '/trash' && hasChildren) && renderEmptyButton()}
+        {(isDeleted && currentUser != null) && renderTrashPageManagementButtons()}
+      </div>
+      {renderModals()}
+    </>
+  );
+};
+
+/**
+ * Wrapper component for using unstated
+ */
+const TrashPageAlertWrapper = (props) => {
+  return createSubscribedElement(TrashPageAlert, props, [AppContainer, PageContainer]);
+};
+
+
+TrashPageAlert.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
+};
+
+export default withTranslation()(TrashPageAlertWrapper);

+ 83 - 51
src/client/js/components/PageCreateModal.jsx

@@ -81,6 +81,14 @@ const PageCreateModal = (props) => {
     window.location.href = encodeURI(urljoin(pageNameInput, '#edit'));
   }
 
+  function ppacInputChangeHandler(value) {
+    setPageNameInput(value);
+  }
+
+  function ppacSubmitHandler() {
+    createInputPage();
+  }
+
   /**
    * access template page
    */
@@ -91,34 +99,41 @@ const PageCreateModal = (props) => {
 
   function renderCreateTodayForm() {
     return (
-      <div className="row form-group">
+      <div className="row">
         <fieldset className="col-12 mb-4">
           <h3 className="grw-modal-head pb-2">{ t("Create today's") }</h3>
-          <div className="d-flex">
-            <div className="create-page-input-row d-flex align-items-center">
-              <span>{userPageRootPath}/</span>
-              <input
-                type="text"
-                className="page-today-input1 form-control text-center"
-                value={todayInput1}
-                onChange={e => onChangeTodayInput1Handler(e.target.value)}
-              />
-              <span className="page-today-suffix">/{now}/</span>
+
+          <div className="d-sm-flex align-items-center justify-items-between">
+
+            <div className="d-flex align-items-center flex-fill flex-wrap flex-lg-nowrap">
+              <div className="d-flex align-items-center">
+                <span>{userPageRootPath}/</span>
+                <input
+                  type="text"
+                  className="page-today-input1 form-control text-center mx-2"
+                  value={todayInput1}
+                  onChange={e => onChangeTodayInput1Handler(e.target.value)}
+                />
+                <span className="page-today-suffix">/{now}/</span>
+              </div>
               <input
                 type="text"
-                className="page-today-input2 form-control"
+                className="page-today-input2 form-control mt-1 mt-lg-0 mx-lg-2 flex-fill"
                 id="page-today-input2"
                 placeholder={t('Input page name (optional)')}
                 value={todayInput2}
                 onChange={e => onChangeTodayInput2Handler(e.target.value)}
               />
             </div>
-            <div className="create-page-button-container">
-              <button type="button" className="btn btn-outline-primary rounded-pill" onClick={createTodayPage}>
+
+            <div className="d-flex justify-content-end mt-1 mt-sm-0">
+              <button type="button" className="grw-btn-create-page btn btn-outline-primary rounded-pill text-nowrap ml-3" onClick={createTodayPage}>
                 <i className="icon-fw icon-doc"></i>{ t('Create') }
               </button>
             </div>
+
           </div>
+
         </fieldset>
       </div>
     );
@@ -126,31 +141,43 @@ const PageCreateModal = (props) => {
 
   function renderInputPageForm() {
     return (
-      <div className="row form-group">
+      <div className="row">
         <fieldset className="col-12 mb-4">
           <h3 className="grw-modal-head pb-2">{ t('Create under') }</h3>
-          <div className="d-flex create-page-input-container">
-            <div className="create-page-input-row d-flex align-items-center">
+
+          <div className="d-sm-flex align-items-center justify-items-between">
+
+            <div className="flex-fill">
               {isReachable
-                // GW-2355 refactor typeahead
-                ? <PagePathAutoComplete crowi={appContainer} initializedPath={path} addTrailingSlash />
+                ? (
+                  <PagePathAutoComplete
+                    crowi={appContainer}
+                    initializedPath={path}
+                    addTrailingSlash
+                    onSubmit={ppacSubmitHandler}
+                    onInputChange={ppacInputChangeHandler}
+                  />
+                )
                 : (
                   <input
                     type="text"
                     value={pageNameInput}
-                    className="page-name-input form-control"
+                    className="form-control flex-fill"
                     placeholder={t('Input page name')}
                     onChange={e => onChangePageNameInputHandler(e.target.value)}
                     required
                   />
                 )}
             </div>
-            <div className="create-page-button-container">
-              <button type="submit" className="btn btn-outline-primary rounded-pill" onClick={createInputPage}>
+
+            <div className="d-flex justify-content-end mt-1 mt-sm-0">
+              <button type="button" className="grw-btn-create-page btn btn-outline-primary rounded-pill text-nowrap ml-3" onClick={createInputPage}>
                 <i className="icon-fw icon-doc"></i>{ t('Create') }
               </button>
             </div>
+
           </div>
+
         </fieldset>
       </div>
     );
@@ -158,53 +185,58 @@ const PageCreateModal = (props) => {
 
   function renderTemplatePageForm() {
     return (
-      <div className="row form-group">
+      <div className="row">
         <fieldset className="col-12">
+
           <h3 className="grw-modal-head pb-2">{ t('template.modal_label.Create template under')}<br />
             <code>{path}</code>
           </h3>
-          <div className="d-flex create-page-input-container">
-            <div className="create-page-input-row d-flex align-items-center">
-
-              <div id="dd-template-type" className="dropdown w-100">
-                <button id="template-type" type="button" className="btn btn-secondary btn dropdown-toggle" data-toggle="dropdown">
-                  {template == null && t('template.option_label.select') }
-                  {template === 'children' && t('template.children.label')}
-                  {template === 'decendants' && t('template.decendants.label')}
-                </button>
-                <div className="dropdown-menu" aria-labelledby="userMenu">
-                  <a className="dropdown-item" type="button" onClick={() => onChangeTemplateHandler('children')}>
-                    { t('template.children.label') } (_template)<br className="d-block d-md-none" />
-                    <small className="text-muted text-wrap">- { t('template.children.desc') }</small>
-                  </a>
-                  <a className="dropdown-item" type="button" onClick={() => onChangeTemplateHandler('decendants')}>
-                    { t('template.decendants.label') } (__template) <br className="d-block d-md-none" />
-                    <small className="text-muted">- { t('template.decendants.desc') }</small>
-                  </a>
-                </div>
-              </div>
 
+          <div className="d-sm-flex align-items-center justify-items-between">
+
+            <div id="dd-template-type" className="dropdown flex-fill">
+              <button id="template-type" type="button" className="btn btn-secondary btn dropdown-toggle w-100" data-toggle="dropdown">
+                {template == null && t('template.option_label.select') }
+                {template === 'children' && t('template.children.label')}
+                {template === 'decendants' && t('template.decendants.label')}
+              </button>
+              <div className="dropdown-menu" aria-labelledby="userMenu">
+                <a className="dropdown-item" type="button" onClick={() => onChangeTemplateHandler('children')}>
+                  { t('template.children.label') } (_template)<br className="d-block d-md-none" />
+                  <small className="text-muted text-wrap">- { t('template.children.desc') }</small>
+                </a>
+                <a className="dropdown-item" type="button" onClick={() => onChangeTemplateHandler('decendants')}>
+                  { t('template.decendants.label') } (__template) <br className="d-block d-md-none" />
+                  <small className="text-muted">- { t('template.decendants.desc') }</small>
+                </a>
+              </div>
             </div>
-            <div className="create-page-button-container">
-              <button type="button" className={`btn btn-outline-primary rounded-pill ${template == null && 'disabled'}`} onClick={createTemplatePage}>
-                <i className="icon-fw icon-doc"></i>
-                <span>{ t('Edit') }</span>
+
+            <div className="d-flex justify-content-end mt-1 mt-sm-0">
+              <button
+                type="button"
+                className={`grw-btn-create-page btn btn-outline-primary rounded-pill text-nowrap ml-3 ${template == null && 'disabled'}`}
+                onClick={createTemplatePage}
+              >
+                <i className="icon-fw icon-doc"></i>{ t('Edit') }
               </button>
             </div>
+
           </div>
+
         </fieldset>
       </div>
     );
   }
   return (
-    <Modal size="lg" isOpen={appContainer.state.isPageCreateModalShown} toggle={appContainer.closePageCreateModal}>
+    <Modal size="lg" isOpen={appContainer.state.isPageCreateModalShown} toggle={appContainer.closePageCreateModal} className="grw-create-page">
       <ModalHeader tag="h4" toggle={appContainer.closePageCreateModal} className="bg-primary text-light">
         { t('New Page') }
       </ModalHeader>
       <ModalBody>
-        {renderCreateTodayForm}
-        {renderInputPageForm}
-        {renderTemplatePageForm}
+        {renderCreateTodayForm()}
+        {renderInputPageForm()}
+        {renderTemplatePageForm()}
       </ModalBody>
     </Modal>
 

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

@@ -0,0 +1,161 @@
+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);

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

@@ -0,0 +1,130 @@
+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 for PagePathAutoComplete
+   * @param {string} value
+   */
+  function ppacInputChangeHandler(value) {
+    setPageNameInput(value);
+  }
+
+  /**
+   * change pageNameInput
+   * @param {string} value
+   */
+  function inputChangeHandler(value) {
+    setPageNameInput(value);
+  }
+
+  async function duplicate() {
+    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);
+    }
+  }
+
+  function ppacSubmitHandler() {
+    duplicate();
+  }
+
+  return (
+    <Modal isOpen={props.isOpen} toggle={props.onClose} className="grw-duplicate-page">
+      <ModalHeader tag="h4" toggle={props.onClose} className="bg-primary text-light">
+        { t('modal_duplicate.label.Duplicate page') }
+      </ModalHeader>
+      <ModalBody>
+        <div className="form-group">
+          <label>{ 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>
+            <div className="flex-fill">
+              {isReachable
+              ? (
+                <PagePathAutoComplete
+                  crowi={appContainer}
+                  initializedPath={path}
+                  addTrailingSlash
+                  onSubmit={ppacSubmitHandler}
+                  onInputChange={ppacInputChangeHandler}
+                />
+              )
+              : (
+                <input
+                  type="text"
+                  value={pageNameInput}
+                  className="form-control"
+                  onChange={e => inputChangeHandler(e.target.value)}
+                  required
+                />
+              )}
+            </div>
+          </div>
+        </div>
+      </ModalBody>
+      <ModalFooter>
+        <ApiErrorMessage errorCode={errorCode} errorMessage={errorMessage} linkPath={path} />
+        <button type="button" className="btn btn-primary" onClick={duplicate}>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,
+
+  isOpen: PropTypes.bool.isRequired,
+  onClose: PropTypes.func.isRequired,
+};
+
+export default withTranslation()(PageDuplicateModallWrapper);

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

@@ -0,0 +1,83 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import { withTranslation } from 'react-i18next';
+
+const ApiErrorMessage = (props) => {
+  const {
+    t, errorCode, errorMessage, linkPath,
+  } = props;
+
+  function reload() {
+    window.location.reload();
+  }
+
+  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>
+          </>
+        );
+      case 'notfound_or_forbidden':
+        return (
+          <strong><i className="icon-fw icon-ban"></i>{ t('page_api_error.notfound_or_forbidden') }</strong>
+        );
+      case 'user_not_admin':
+        return (
+          <strong><i className="icon-fw icon-ban"></i>{ t('page_api_error.user_not_admin') }</strong>
+        );
+      case 'outdated':
+        return (
+          <>
+            <strong><i className="icon-fw icon-bulb"></i> { t('page_api_error.outdated') }</strong>
+            <a className="btn-link" onClick={reload}>
+              <i className="fa fa-angle-double-right"></i> { t('Load latest') }
+            </a>
+          </>
+        );
+      case 'invalid_path':
+        return (
+          <strong><i className="icon-fw icon-ban"></i> Invalid path</strong>
+        );
+      case 'unknown':
+        return (
+          <strong><i className="icon-fw icon-ban"></i> Unknown error occured</strong>
+        );
+      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;
+
+};
+
+ApiErrorMessage.propTypes = {
+  t:            PropTypes.func.isRequired, //  i18next
+
+  errorCode:    PropTypes.string,
+  errorMessage: PropTypes.string,
+  linkPath:     PropTypes.string,
+};
+
+export default withTranslation()(ApiErrorMessage);

+ 38 - 40
src/client/js/components/PagePathAutoComplete.jsx

@@ -5,65 +5,63 @@ import { pathUtils } from 'growi-commons';
 
 import SearchTypeahead from './SearchTypeahead';
 
-export default class PagePathAutoComplete extends React.Component {
+const PagePathAutoComplete = (props) => {
 
-  constructor(props) {
+  const {
+    addTrailingSlash, onSubmit, onInputChange, initializedPath,
+  } = props;
 
-    super(props);
+  function inputChangeHandler(pages) {
+    if (onInputChange == null) {
+      return;
+    }
+    const page = pages[0]; // should be single page selected
 
-    this.state = {
-    };
-
-    this.crowi = this.props.crowi;
-
-    this.onSubmit = this.onSubmit.bind(this);
-    this.getKeywordOnInit = this.getKeywordOnInit.bind(this);
-  }
-
-  componentDidMount() {
-  }
-
-  componentWillUnmount() {
+    if (page != null) {
+      onInputChange(page.path);
+    }
   }
 
-  onSubmit(query) {
-    // get the closest form element
-    const elem = this.rootDom;
-    const form = elem.closest('form');
-    // submit with jQuery
-    $(form).submit();
+  function submitHandler() {
+    if (onSubmit == null) {
+      return;
+    }
+    onSubmit();
   }
 
-  getKeywordOnInit(path) {
-    return this.props.addTrailingSlash
+  function getKeywordOnInit(path) {
+    return addTrailingSlash
       ? pathUtils.addTrailingSlash(path)
       : pathUtils.removeTrailingSlash(path);
   }
 
-  render() {
-    return (
-      <div ref={(c) => { this.rootDom = c }}>
-        <SearchTypeahead
-          ref={this.searchTypeaheadDom}
-          crowi={this.crowi}
-          onSubmit={this.onSubmit}
-          inputName="new_path"
-          emptyLabelExceptError={null}
-          placeholder="Input page path"
-          keywordOnInit={this.getKeywordOnInit(this.props.initializedPath)}
-        />
-      </div>
-    );
-  }
+  return (
+    <SearchTypeahead
+      crowi={props.crowi}
+      onSubmit={submitHandler}
+      onChange={inputChangeHandler}
+      onInputChange={props.onInputChange}
+      inputName="new_path"
+      emptyLabelExceptError={null}
+      placeholder="Input page path"
+      keywordOnInit={getKeywordOnInit(initializedPath)}
+    />
+  );
 
-}
+};
 
 PagePathAutoComplete.propTypes = {
   crowi:            PropTypes.object.isRequired,
   initializedPath:  PropTypes.string,
   addTrailingSlash: PropTypes.bool,
+
+  onSubmit:         PropTypes.func,
+  onInputChange:    PropTypes.func,
 };
 
 PagePathAutoComplete.defaultProps = {
   initializedPath: '/',
+  addTrailingSlash: true,
 };
+
+export default PagePathAutoComplete;

+ 172 - 0
src/client/js/components/PageRenameModal.jsx

@@ -0,0 +1,172 @@
+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 ApiErrorMessage from './PageManagement/ApiErrorMessage';
+
+const PageRenameModal = (props) => {
+  const {
+    t, appContainer, pageContainer,
+  } = props;
+
+  const { path } = pageContainer.state;
+
+  const { crowi } = appContainer.config;
+
+  const [pageNameInput, setPageNameInput] = useState(path);
+  const [errorCode, setErrorCode] = useState(null);
+  const [errorMessage, setErrorMessage] = useState(null);
+
+  const [isRenameRecursively, SetIsRenameRecursively] = useState(true);
+  const [isRenameRedirect, SetIsRenameRedirect] = useState(false);
+  const [isRenameMetadata, SetIsRenameMetadata] = useState(false);
+
+  function changeIsRenameRecursivelyHandler() {
+    SetIsRenameRecursively(!isRenameRecursively);
+  }
+
+  function changeIsRenameRedirectHandler() {
+    SetIsRenameRedirect(!isRenameRedirect);
+  }
+
+  function changeIsRenameMetadataHandler() {
+    SetIsRenameMetadata(!isRenameMetadata);
+  }
+
+  /**
+   * change pageNameInput
+   * @param {string} value
+   */
+  function inputChangeHandler(value) {
+    setPageNameInput(value);
+  }
+
+  async function rename() {
+    try {
+      setErrorCode(null);
+      setErrorMessage(null);
+
+      const response = await pageContainer.rename(
+        pageNameInput,
+        isRenameRecursively,
+        isRenameRedirect,
+        isRenameMetadata,
+      );
+      const { page } = response;
+      window.location.href = encodeURI(`${page.path}?renamed=${path}`);
+    }
+    catch (err) {
+      setErrorCode(err.code);
+      setErrorMessage(err.message);
+    }
+  }
+
+  return (
+    <Modal isOpen={props.isOpen} toggle={props.onClose} className="grw-create-page">
+      <ModalHeader tag="h4" toggle={props.onClose} className="bg-primary text-light">
+        { t('modal_rename.label.Move/Rename page') }
+      </ModalHeader>
+      <ModalBody>
+        <div className="form-group">
+          <label>{ t('modal_rename.label.Current page name') }</label><br />
+          <code>{ path }</code>
+        </div>
+        <div className="form-group">
+          <label htmlFor="newPageName">{ t('modal_rename.label.New page name') }</label><br />
+          <div className="input-group">
+            <div className="input-group-prepend">
+              <span className="input-group-text">{crowi.url}</span>
+            </div>
+            <div className="flex-fill">
+              <input
+                type="text"
+                value={pageNameInput}
+                className="form-control"
+                onChange={e => inputChangeHandler(e.target.value)}
+                required
+              />
+            </div>
+          </div>
+        </div>
+        <div className="custom-control custom-checkbox custom-checkbox-warning">
+          <input
+            className="custom-control-input"
+            name="recursively"
+            id="cbRenameRecursively"
+            type="checkbox"
+            checked={isRenameRecursively}
+            onChange={changeIsRenameRecursivelyHandler}
+          />
+          <label className="custom-control-label" htmlFor="cbRenameRecursively">
+            { t('modal_rename.label.Recursively') }
+            <p className="form-text text-muted mt-0">{ t('modal_rename.help.recursive') }</p>
+          </label>
+        </div>
+
+        <div className="custom-control custom-checkbox custom-checkbox-success">
+          <input
+            className="custom-control-input"
+            name="create_redirect"
+            id="cbRenameRedirect"
+            type="checkbox"
+            checked={isRenameRedirect}
+            onChange={changeIsRenameRedirectHandler}
+          />
+          <label className="custom-control-label" htmlFor="cbRenameRedirect">
+            { t('modal_rename.label.Redirect') }
+            <p className="form-text text-muted mt-0">{ t('modal_rename.help.redirect') }</p>
+          </label>
+        </div>
+
+        <div className="custom-control custom-checkbox custom-checkbox-primary">
+          <input
+            className="custom-control-input"
+            name="remain_metadata"
+            id="cbRenameMetadata"
+            type="checkbox"
+            checked={isRenameMetadata}
+            onChange={changeIsRenameMetadataHandler}
+          />
+          <label className="custom-control-label" htmlFor="cbRenameMetadata">
+            { t('modal_rename.label.Do not update metadata') }
+            <p className="form-text text-muted mt-0">{ t('modal_rename.help.metadata') }</p>
+          </label>
+        </div>
+      </ModalBody>
+      <ModalFooter>
+        <ApiErrorMessage errorCode={errorCode} errorMessage={errorMessage} linkPath={path} />
+        <button type="button" className="btn btn-primary" onClick={rename}>Rename</button>
+      </ModalFooter>
+    </Modal>
+  );
+};
+
+/**
+ * Wrapper component for using unstated
+ */
+const PageRenameModalWrapper = (props) => {
+  return createSubscribedElement(PageRenameModal, props, [AppContainer, PageContainer]);
+};
+
+
+PageRenameModal.propTypes = {
+  t: PropTypes.func.isRequired, //  i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
+
+  isOpen: PropTypes.bool.isRequired,
+  onClose: PropTypes.func.isRequired,
+
+  path: PropTypes.string.isRequired,
+};
+
+export default withTranslation()(PageRenameModalWrapper);

+ 104 - 0
src/client/js/components/PutbackPageModal.jsx

@@ -0,0 +1,104 @@
+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 PutBackPageModal = (props) => {
+  const {
+    t, isOpen, onClose, pageContainer, path,
+  } = props;
+
+  const [errorCode, setErrorCode] = useState(null);
+  const [errorMessage, setErrorMessage] = useState(null);
+
+  const [isPutbackRecursively, setIsPutbackRecursively] = useState(true);
+
+  function changeIsPutbackRecursivelyHandler() {
+    setIsPutbackRecursively(!isPutbackRecursively);
+  }
+
+  async function putbackPage() {
+    setErrorCode(null);
+    setErrorMessage(null);
+
+    try {
+      const response = await pageContainer.revertRemove(isPutbackRecursively);
+      const putbackPagePath = response.page.path;
+      window.location.href = encodeURI(putbackPagePath);
+    }
+    catch (err) {
+      setErrorCode(err.code);
+      setErrorMessage(err.message);
+    }
+  }
+
+  async function putbackPageButtonHandler() {
+    putbackPage();
+  }
+
+  return (
+    <Modal isOpen={isOpen} toggle={onClose} className="grw-create-page">
+      <ModalHeader tag="h4" toggle={onClose} className="bg-info text-light">
+        <i className="icon-action-undo mr-2" aria-hidden="true"></i> { t('modal_putback.label.Put Back Page') }
+      </ModalHeader>
+      <ModalBody>
+        <div className="form-group">
+          <label>{t('modal_putback.label.Put Back Page')}:</label><br />
+          <code>{path}</code>
+        </div>
+        <div className="custom-control custom-checkbox custom-checkbox-warning">
+          <input
+            className="custom-control-input"
+            id="cbPutBackRecursively"
+            type="checkbox"
+            checked={isPutbackRecursively}
+            onChange={changeIsPutbackRecursivelyHandler}
+          />
+          <label htmlFor="cbPutBackRecursively" className="custom-control-label">
+            { t('modal_putback.label.recursively') }
+          </label>
+          <p className="form-text text-muted mt-0">
+            <code>{ path }</code>{ t('modal_putback.help.recursively') }
+          </p>
+        </div>
+      </ModalBody>
+      <ModalFooter>
+        <ApiErrorMessage errorCode={errorCode} errorMessage={errorMessage} linkPath={path} />
+        <button type="button" className="btn btn-info" onClick={putbackPageButtonHandler}>
+          <i className="icon-action-undo mr-2" aria-hidden="true"></i> { t('Put Back') }
+        </button>
+      </ModalFooter>
+    </Modal>
+  );
+
+};
+
+/**
+ * Wrapper component for using unstated
+ */
+const PutBackPageModalWrapper = (props) => {
+  return createSubscribedElement(PutBackPageModal, props, [PageContainer]);
+};
+
+PutBackPageModal.propTypes = {
+  t: PropTypes.func.isRequired, //  i18next
+  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
+
+  isOpen: PropTypes.bool.isRequired,
+  onClose: PropTypes.func.isRequired,
+
+  path: PropTypes.string.isRequired,
+};
+
+
+export default withTranslation()(PutBackPageModalWrapper);

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

@@ -1,7 +1,4 @@
 /* eslint-disable react/jsx-filename-extension */
-
-import { pathUtils } from 'growi-commons';
-
 require('jquery.cookie');
 
 require('./thirdparty-js/waves');
@@ -182,7 +179,6 @@ Crowi.highlightSelectedSection = function(hash) {
 
 $(() => {
   const appContainer = window.appContainer;
-  const websocketContainer = appContainer.getContainer('WebsocketContainer');
   const config = appContainer.getConfig();
 
   const pageId = $('#content-main').data('page-id');
@@ -190,7 +186,6 @@ $(() => {
   // const revisionCreatedAt = $('#content-main').data('page-revision-created');
   // const currentUser = $('#content-main').data('current-user');
   const isSeen = $('#content-main').data('page-is-seen');
-  const pagePath = $('#content-main').data('path');
   const isSavedStatesOfTabChanges = config.isSavedStatesOfTabChanges;
 
   $('[data-toggle="popover"]').popover();
@@ -218,258 +213,7 @@ $(() => {
     $(this).select();
   });
 
-
-  // TODO GW-2355 remove this after refactoring
-  $('#create-page').on('shown.bs.modal', (e) => {
-    // quick hack: replace from server side rendering "date" to client side "date"
-    const today = new Date();
-    const month = (`0${today.getMonth() + 1}`).slice(-2);
-    const day = (`0${today.getDate()}`).slice(-2);
-    const dateString = `${today.getFullYear()}/${month}/${day}`;
-    $('#create-page-today .page-today-suffix').text(`/${dateString}/`);
-    $('#create-page-today .page-today-input2').data('prefix', `/${dateString}/`);
-
-    // focus
-    $('#create-page-today .page-today-input2').eq(0).focus();
-  });
-
-  $('#create-page-today').submit(function(e) {
-    let prefix1 = $('input.page-today-input1', this).data('prefix');
-    let prefix2 = $('input.page-today-input2', this).data('prefix');
-    const input1 = $('input.page-today-input1', this).val();
-    const input2 = $('input.page-today-input2', this).val();
-    if (input1 === '') {
-      prefix1 = 'メモ';
-    }
-    if (input2 === '') {
-      prefix2 = prefix2.slice(0, -1);
-    }
-    window.location.href = `${prefix1 + input1 + prefix2 + input2}#edit`;
-    return false;
-  });
-
-  $('#create-page-under-tree').submit(function(e) {
-    let name = $('input', this).val();
-    if (!name.match(/^\//)) {
-      name = `/${name}`;
-    }
-    if (name.match(/.+\/$/)) {
-      name = name.substr(0, name.length - 1);
-    }
-    // TODO: remove by GW-2278
-    window.location.href = `${pathUtils.encodePagePath(name)}#edit`;
-    return false;
-  });
-
-  // rename
-  $('#renamePage').on('shown.bs.modal', (e) => {
-    $('#renamePage #newPageName').focus();
-    $('#renamePage .msg').hide();
-  });
-  $('#renamePageForm').submit(function(e) {
-    // create name-value map
-    const nameValueMap = {};
-    $(this).serializeArray().forEach((obj) => {
-      nameValueMap[obj.name] = obj.value; // nameValueMap.new_path is renamed page path
-    });
-    nameValueMap.socketClientId = websocketContainer.getSocketClientId();
-
-    $.ajax({
-      type: 'POST',
-      url: '/_api/pages.rename',
-      data: nameValueMap,
-      dataType: 'json',
-    })
-      .done((res) => {
-      // error
-        if (!res.ok) {
-          const linkPath = pathUtils.normalizePath(nameValueMap.new_path);
-          $('#renamePage .msg').hide();
-          $(`#renamePage .msg-${res.code}`).show();
-          $('#renamePage #linkToNewPage').html(`
-          <a href="${linkPath}">${linkPath} <i class="icon-login"></i></a>
-        `);
-        }
-        else {
-          const page = res.page;
-          window.location.href = `${page.path}?renamed=${pagePath}`;
-        }
-      });
-
-    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();
-  });
-  $('#empty-trash-form').submit((e) => {
-    // create name-value map
-    const nameValueMap = {};
-    $('#empty-trash-form').serializeArray().forEach((obj) => {
-      nameValueMap[obj.name] = obj.value;
-    });
-    $.ajax({
-      type: 'DELETE',
-      url: '/_api/v3/pages/empty-trash',
-      data: nameValueMap,
-      dataType: 'json',
-    }).done((res) => {
-      window.location.href = '/trash';
-    }).fail((jqXHR, textStatus, errorThrown) => {
-      $('#emptyTrash .msg').hide();
-      $('#emptyTrash .msg-unknown').show();
-    });
-
-    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) => {
-    $('#putBackPage .msg').hide();
-  });
-  $('#revert-delete-page-form').submit((e) => {
-    $.ajax({
-      type: 'POST',
-      url: '/_api/pages.revertRemove',
-      data: $('#revert-delete-page-form').serialize(),
-      dataType: 'json',
-    }).done((res) => {
-      // error
-      if (!res.ok) {
-        $('#putBackPage .msg').hide();
-        $(`#putBackPage .msg-${res.code}`).show();
-      }
-      else {
-        const page = res.page;
-        window.location.href = page.path;
-      }
-    });
-
-    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');
-
-    $('body').addClass('on-edit');
-    $('body').addClass('builtin-editor');
-
-    const path = $('.content-main').data('path');
-    if (path !== '/' && $('.content-main').data('page-id') === '') {
-      const upperPage = path.substr(0, path.length - 1);
-      $.get('/_api/pages.get', { path: upperPage }, (res) => {
-        if (res.ok && res.page) {
-          $('#portal-warning-modal').modal('show');
-        }
-      });
-    }
-  });
-  $('#portal-form-close').on('click', (e) => {
-    $('#edit').removeClass('active');
-    $('body').removeClass('on-edit');
-    $('body').removeClass('builtin-editor');
-    window.location.hash = '#';
-  });
-
   if (pageId) {
-    // for Crowi Template LangProcessor
-    $('.template-create-button', $('#revision-body')).on('click', function() {
-      const path = $(this).data('path');
-      const templateId = $(this).data('template');
-      const template = $(`#${templateId}`).html();
-
-      const editorContainer = appContainer.getContainer('EditorContainer');
-      editorContainer.saveDraft(path, template);
-      window.location.href = `${path}#edit`;
-    });
 
     if (!isSeen) {
       $.post('/_api/pages.seen', { page_id: pageId }, (res) => {

+ 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,
@@ -450,6 +452,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);
   }
 

+ 66 - 2
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;
@@ -32,7 +33,6 @@ export default class PageContainer extends Container {
 
     const revisionId = mainContent.getAttribute('data-page-revision-id');
     const path = decodeURI(mainContent.getAttribute('data-path'));
-
     this.state = {
       // local page data
       markdown: null, // will be initialized after initStateMarkdown()
@@ -48,8 +48,11 @@ export default class PageContainer extends Container {
       createdAt: mainContent.getAttribute('data-page-created-at'),
       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')),
       templateTagData: mainContent.getAttribute('data-template-tags') || null,
 
       // latest(on remote) information
@@ -85,6 +88,20 @@ export default class PageContainer extends Container {
         isSubnavCompact: scrollThresForCompact < currentYOffset,
       });
     });
+
+    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);
+        }
+      });
+    }
+
   }
 
   /**
@@ -294,6 +311,53 @@ 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(),
+    });
+
+  }
+
+  revertRemove(isRecursively) {
+    const websocketContainer = this.appContainer.getContainer('WebsocketContainer');
+
+    // control flag
+    const recursively = isRecursively ? true : null;
+
+    return this.appContainer.apiPost('/pages.revertRemove', {
+      recursively,
+      page_id: this.state.pageId,
+      socketClientId: websocketContainer.getSocketClientId(),
+    });
+  }
+
+  rename(pageNameInput, isRenameRecursively, isRenameRedirect, isRenameMetadata) {
+    const websocketContainer = this.appContainer.getContainer('WebsocketContainer');
+    const isRecursively = isRenameRecursively ? true : null;
+    const isRedirect = isRenameRedirect ? true : null;
+    const isRemain = isRenameMetadata ? true : null;
+
+    return this.appContainer.apiPost('/pages.rename', {
+      recursively: isRecursively,
+      page_id: this.state.pageId,
+      revision_id: this.state.revisionId,
+      new_path: pageNameInput,
+      create_redirect: isRedirect,
+      remain_metadata: isRemain,
+      socketClientId: websocketContainer.getSocketClientId(),
+    });
+  }
+
   showSuccessToastr() {
     toastr.success(undefined, 'Saved successfully', {
       closeButton: true,

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

+ 14 - 60
src/client/styles/scss/_create-page.scss

@@ -1,61 +1,15 @@
-.modal.create-page {
-  .modal-body {
-    //TODO remove legend
-    legend {
-      margin-bottom: 10px;
-    }
-
-    form,
-    #template-form {
-      // layout
-      .create-page-input-container {
-        .create-page-input-row {
-          flex: 1;
-        }
-        .create-page-button-container {
-          margin-left: 15px;
-          .btn {
-            min-width: 105px;
-          }
-        }
-
-        // change layout by screen size
-        @include media-breakpoint-down(md) {
-          flex-direction: column;
-          .create-page-button-container {
-            margin-top: 10px;
-            text-align: right;
-          }
-        }
-      }
-
-      .page-today-prefix {
-      }
-      .page-today-input1 {
-        width: 60px;
-        padding-right: 2px;
-        padding-left: 2px;
-        margin-right: 5px;
-        margin-left: 5px;
-      }
-      .page-today-suffix {
-      }
-      .page-today-input2 {
-        flex: 1;
-        margin-left: 5px;
-      }
-
-      .page-name-input {
-        flex: 1;
-        input {
-          min-width: 300px; // Workaround to display placeholder.
-          //   cf https://github.com/ericgio/react-bootstrap-typeahead/issues/256
-        }
-      }
-
-      .create-page-under-tree-label code {
-        font-family: $font-family-monospace-not-strictly;
-      }
-    }
-  } // .modal-body
+.grw-create-page {
+  .page-today-input1 {
+    width: 60px;
+  }
+  .page-today-input2 {
+  }
+
+  .grw-btn-create-page {
+    min-width: 90px;
+  }
+
+  .create-page-under-tree-label code {
+    font-family: $font-family-monospace-not-strictly;
+  }
 }

+ 1 - 1
src/server/routes/page.js

@@ -1296,7 +1296,7 @@ module.exports = function(crowi, app) {
     const socketClientId = req.body.socketClientId || undefined;
 
     // get recursively flag
-    const isRecursively = (req.body.recursively !== undefined);
+    const isRecursively = (req.body.recursively != null);
 
     let page;
     try {

+ 0 - 4
src/server/views/layout-growi/page.html

@@ -55,8 +55,4 @@
   <div id="presentation-layer" class="fullscreen-layer">
     <div id="presentation-container"></div>
   </div>
-
-  <div id="crowi-modals">
-    {% include '../widget/page_modals.html' %}
-  </div>
 {% endblock %}

+ 0 - 3
src/server/views/layout-growi/page_list.html

@@ -53,7 +53,4 @@
   <div id="presentation-layer" class="fullscreen-layer">
     <div id="presentation-container"></div>
   </div>
-  <div id="crowi-modals">
-    {% include '../widget/page_modals.html' %}
-  </div>
 {% endblock %}

+ 0 - 4
src/server/views/layout-kibela/page.html

@@ -53,8 +53,4 @@
   <div id="presentation-layer" class="fullscreen-layer">
     <div id="presentation-container"></div>
   </div>
-
-  <div id="crowi-modals">
-    {% include '../widget/page_modals.html' %}
-  </div>
 {% endblock %}

+ 0 - 3
src/server/views/layout-kibela/page_list.html

@@ -51,7 +51,4 @@
 <div id="presentation-layer" class="fullscreen-layer">
   <div id="presentation-container"></div>
 </div>
-<div id="crowi-modals">
-  {% include '../widget/page_modals.html' %}
-</div>
 {% endblock %}

+ 3 - 12
src/server/views/layout/layout.html

@@ -89,13 +89,7 @@
     {# Navbar Right #}
     <ul class="navbar-nav ml-auto">
       {% if user %}
-        <!-- TODO GW-79 enable after refactoring  <li id="create-page-button" class="nav-item d-none d-md-block"></li> -->
-        <li class="nav-item d-none d-md-block">
-          <a class="nav-link create-page px-4" href="#" data-target="#create-page" data-toggle="modal">
-            <i class="icon-pencil mr-2"></i>
-            <span>{{ t('New') }}</span>
-          </a>
-        </li>
+        <li id="create-page-button" class="nav-item d-none d-md-block"></li>
         {% if isSearchServiceConfigured() %}
           <li class="nav-item d-md-none">
             <a type="button" class="nav-link px-4" data-target="#grw-search-top-collapse" data-toggle="collapse">
@@ -140,17 +134,14 @@
 
 <div class="grw-fixed-controls-container d-md-none d-edit-none animated fadeInUp faster">
   <div class="grw-fixed-controls-button-container rounded-circle">
-    <!-- TODO GW-79 enable after refactoring <div id='create-page-button-icon'></div> -->
-    <button class="btn btn-lg btn-primary rounded-circle waves-effect waves-light" type="button" data-target="#create-page" data-toggle="modal">
-      <i class="icon-pencil"></i>
-    </button>
+    <div id='create-page-button-icon'></div>
   </div>
 </div>
 
 <!-- /#staff-credit -->
 <div id="staff-credit"></div>
 
-<!-- TODO GW-79 enable after refactoring <div id="page-create-modal"></div> -->
+<div id="page-create-modal"></div>
 {% include '../modal/shortcuts.html' %}
 
 {% block body_end %}

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

@@ -1,50 +0,0 @@
-{% 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 -->

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

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

@@ -1,46 +0,0 @@
-  <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 -->

+ 0 - 47
src/server/views/modal/empty_trash.html

@@ -1,47 +0,0 @@
-<div class="modal" id="emptyTrash">
-  <div class="modal-dialog">
-    <div class="modal-content">
-
-    <form role="form" id="empty-trash-form" onsubmit="return false;">
-
-      <div class="modal-header bg-danger text-light">
-        <div class="modal-title">
-          <i class="icon-fw icon-trash"></i>  {{ t('modal_empty.empty_the_trash') }}
-        </div>
-        <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
-      </div>
-      <div class="modal-body">
-        <ul>
-          {% for data in pages %}
-            {% if pagePropertyName %}
-              {% set deletePage = data[pagePropertyName] %}
-            {% else %}
-              {% set deletePage = data %}
-            {% endif %}
-            <li>
-              <img src="{{ deletePage.lastUpdateUser|picture }}" class="picture img-circle">
-              <a href="{{ deletePage.path }}"
-                class="page-list-link"
-                data-path="{{ deletePage.path }}">{{ decodeURIComponent(deletePage.path) }}
-              </a>
-            </li>
-          {% endfor %}
-        </ul>
-      </div>
-      <div class="modal-footer">
-        <div class="d-flex justify-content-end">
-          {% include '../widget/modal/page-api-error-messages.html' %}
-          <div>
-            <input type="hidden" name="_csrf" value="{{ csrf() }}">
-            <button type="submit" class="m-l-10 btn btn-danger delete-button">
-              <i class="icon-trash" aria-hidden="true"></i>
-              Empty
-            </button>
-          </div>
-        </div>
-      </div><!-- /.modal-footer -->
-
-    </form>
-    </div><!-- /.modal-content -->
-  </div><!-- /.modal-dialog -->
-</div><!-- /.modal -->

+ 0 - 45
src/server/views/modal/put_back.html

@@ -1,45 +0,0 @@
-<div class="modal" id="putBackPage">
-  <div class="modal-dialog">
-    <div class="modal-content">
-
-      <form role="form" id="revert-delete-page-form" onsubmit="return false;">
-
-        <div class="modal-header bg-info text-light">
-          <div class="modal-title"><i class="icon-action-undo"></i> {{ t('modal_putback.label.Put Back 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="">Put back page:</label><br>
-            <code>{{ page.path }}</code>
-          </div>
-          <div class="custom-control custom-checkbox custom-checkbox-warning">
-            <input class="custom-control-input" name="recursively" id="cbPutbackRecursively" value="1" type="checkbox" checked>
-            <label class="custom-control-label" for="cbPutbackRecursively">
-              {{ t('modal_putback.label.recursively') }}
-              <p class="form-text text-muted mt-0">{{ t('modal_putback.help.recursively', page.path) }}</p>
-            </label>
-          </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() }}">
-              <button type="submit" class="btn btn-sm btn-info putBack-button">
-                <i class="icon-action-undo" aria-hidden="true"></i>
-                {{ t('Put Back') }}
-              </button>
-            </div>
-          </div>
-        </div>
-
-      </form>
-    </div>
-    <!-- /.modal-content -->
-  </div>
-  <!-- /.mod<div class="modal" id="deletePage">al-dialog -->
-</div>
-<!-- /.modal -->

+ 0 - 77
src/server/views/modal/rename.html

@@ -1,77 +0,0 @@
-  <div class="modal" id="renamePage">
-    <div class="modal-dialog">
-      <div class="modal-content">
-
-      <form role="form" id="renamePageForm" onsubmit="return false;">
-
-        <div class="modal-header bg-primary text-light">
-          <div class="modal-title">{{ t('modal_rename.label.Move/Rename 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_rename.label.Current page name') }}</label><br>
-            <code>{{ page.path }}</code>
-          </div>
-          <div class="form-group">
-            <label for="newPageName">{{ t('modal_rename.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="rename-page-name-input" class="page-name-input flex-fill"></div>
-              {% else %}
-              <input type="text" class="form-control" name="new_path" id="newPageName" value="{{ page.path }}">
-              {% endif %}
-            </div>
-          </div>
-
-          <hr>
-
-          {% if page.grant != 2 %}
-          <div class="custom-control custom-checkbox custom-checkbox-warning">
-            <input class="custom-control-input" name="recursively" id="cbRenameRecursively" value="1" type="checkbox" checked >
-            <label class="custom-control-label" for="cbRenameRecursively">
-              {{ t('modal_rename.label.Recursively') }}
-              <p class="form-text text-muted mt-0">{{ t('modal_rename.help.recursive', page.path) }}</p>
-            </label>
-          </div>
-          {% endif %}
-
-          <div class="custom-control custom-checkbox custom-checkbox-success">
-            <input class="custom-control-input" name="create_redirect" id="cbRenameRedirect" value="1" type="checkbox">
-            <label class="custom-control-label" for="cbRenameRedirect">
-              {{ t('modal_rename.label.Redirect') }}
-              <p class="form-text text-muted mt-0">{{ t('modal_rename.help.redirect', page.path) }}</p>
-            </label>
-          </div>
-
-          <div class="custom-control custom-checkbox custom-checkbox-primary">
-            <input class="custom-control-input" name="remain_metadata" id="cbRenameMetadata" value="1" type="checkbox">
-            <label class="custom-control-label" for="cbRenameMetadata">
-              {{ t('modal_rename.label.Do not update metadata') }}
-              <p class="form-text text-muted mt-0">{{ t('modal_rename.help.metadata') }}</p>
-            </label>
-          </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() }}">
-            </div>
-          </div>
-          <div class="d-flex justify-content-end">
-            <button type="submit" class="btn btn-primary">Rename</button>
-          </div>
-        </div>
-
-      </form>
-      </div><!-- /.modal-content -->
-    </div><!-- /.modal-dialog -->
-  </div><!-- /.modal -->

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

@@ -1,24 +0,0 @@
-<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>
-  </span>
-  <span class="text-danger msg msg-user_not_admin">
-    <strong><i class="icon-fw icon-ban"></i>{{ t('page_api_error.user_not_admin') }}</strong>
-  </span>
-  <span class="text-danger msg msg-already_exists">
-    <strong><i class="icon-fw icon-ban"></i>{{ t('page_api_error.already_exists') }}</strong><br>
-    <small id="linkToNewPage"></small>
-  </span>
-  <span class="text-warning msg msg-outdated">
-    <strong><i class="icon-fw icon-bulb"></i> {{ t('page_api_error.outdated') }}</strong>
-    <a href="javascript:location.reload();">
-      <i class="fa fa-angle-double-right"></i> {{ t('Load latest') }}
-    </a>
-  </span>
-  <span class="text-danger msg msg-invalid_path">
-    <strong><i class="icon-fw icon-ban"></i> Invalid path</strong>
-  </span>
-  <span class="text-danger msg msg-unknown">
-    <strong><i class="icon-fw icon-ban"></i> Unknown error occured</strong>
-  </span>
-</p>

+ 2 - 25
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 %}
@@ -82,25 +77,7 @@
     {% endif %}
 
     {% if isTrashPage() %}
-    <div class="alert alert-warning py-3 px-4 d-flex align-items-center justify-content-between">
-      <div>
-        This page is in the trash <i class="icon-trash" aria-hidden="true"></i>.
-        {% if page.isDeleted() %}
-        <br>Deleted by <img src="{{ page.lastUpdateUser|picture }}" class="picture picture-sm rounded-circle"> {{ page.lastUpdateUser.name }} at {{ page.updatedAt|datetz('Y-m-d H:i:s') }}
-        {% endif %}
-      </div>
-      {% if user and user.admin and req.path == '/trash' and pages.length > 0 %}
-      <div>
-        <button href="#" class="btn btn-danger rounded-pill btn-sm" data-target="#emptyTrash" data-toggle="modal"><i class="icon-trash" aria-hidden="true"></i>{{ t('modal_empty.empty_the_trash') }}</button>
-      </div>
-      {% endif %}
-      {% if page.isDeleted() and user %}
-      <div>
-        <button href="#" class="btn btn-outline-secondary rounded-pill btn-sm mr-2" data-target="#putBackPage" data-toggle="modal"><i class="icon-action-undo" aria-hidden="true"></i> {{ t('Put Back') }}</button>
-        <button href="#" class="btn btn-danger rounded-pill btn-sm mr-2" {% if !user.canDeleteCompletely(page.creator._id) %} disabled="disabled" {% endif %} data-target="#deletePage" data-toggle="modal"><i class="icon-fire" aria-hidden="true"></i> {{ t('Delete Completely') }}</button>
-      </div>
-      {% endif %}
-    </div>
+      <div id="trash-page-alert"></div>
     {% endif %}
   </div>
 </div>

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

@@ -11,16 +11,22 @@
   data-page-has-draft-on-hackmd="{% if hasDraftOnHackmd %}{{ hasDraftOnHackmd.toString() }}{% endif %}"
   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 %}"
   data-page-creator="{% if page %}{{ page.creator|json }}{% endif %}"
   data-page-updated-at="{% if page %}{{ page.updatedAt|datetz('Y/m/d H:i:s') }}{% endif %}"
+  data-page-has-children="{% if pages.length > 0 %}true{% else %}false{% endif %}"
   >
 {% else %}
 <div id="content-main" class="content-main"
   data-path="{{ encodeURI(path) }}"
   data-current-user="{% if user %}{{ user._id.toString() }}{% endif %}"
   data-slack-channels="{{ slack|default('') }}"
+  data-page-is-deleted="{% if page.isDeleted() %}true{% else %}false{% endif %}"
+  data-page-has-children="{% if pages.length > 0 %}true{% else %}false{% endif %}"
   >
 {% endif %}
 

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

@@ -1,6 +0,0 @@
-{% 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' %}

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

@@ -65,44 +65,7 @@
 
   <!-- icon-options-vertical -->
   {% if !isTrashPage() %}
-    {% if page.isTopPage() %}
-    <li class="nav-item dropdown">
-      <a
-        {% if user %} role="button" class="nav-link dropdown-toggle dropdown-toggle-no-caret" href="#" data-toggle="dropdown" {% endif %}
-        {% if not user %}
-          class="nav-link dropdown-toggle dropdown-toggle-disabled dropdown-toggle-no-caret"
-          data-toggle="tooltip" data-placement="top" data-container="body" title="{{ t('Not available for guest') }}"
-        {% endif %}
-      >
-        <i class="icon-options-vertical"></i>
-      </a>
-      <div class="dropdown-menu dropdown-menu-right">
-        <a class="dropdown-item" href="#" data-target="#create-template" data-toggle="modal"><i class="icon-fw icon-magic-wand"></i> {{ t('template.option_label.create/edit') }}</a>
-      </div>
-    </li>
-    {% else %}
-    <li class="nav-item dropdown">
-      <a
-        {% if user %} role="button" class="nav-link dropdown-toggle dropdown-toggle-no-caret" href="#" data-toggle="dropdown" {% endif %}
-        {% if not user %}
-          class="nav-link dropdown-toggle dropdown-toggle-disabled dropdown-toggle-no-caret"
-          data-toggle="tooltip" data-placement="top" data-container="body" title="{{ t('Not available for guest') }}"
-        {% endif %}
-      >
-        <i class="icon-options-vertical"></i>
-      </a>
-      <div class="dropdown-menu dropdown-menu-right">
-        <a class="dropdown-item" href="#" data-target="#renamePage" data-toggle="modal"><i class="icon-fw icon-action-redo"></i> {{ t('Move/Rename') }}</a>
-        <a class="dropdown-item" href="#" data-target="#duplicatePage" data-toggle="modal"><i class="icon-fw icon-docs"></i> {{ t('Duplicate') }}</a>
-        <div class="dropdown-divider"></div>
-        <a class="dropdown-item" href="#" data-target="#create-template" data-toggle="modal"><i class="icon-fw icon-magic-wand"></i> {{ t('template.option_label.create/edit') }}</a>
-        {% if isDeletablePage() %}
-          <div class="dropdown-divider"></div>
-          <a class="dropdown-item" href="#" data-target="#deletePage" data-toggle="modal"><i class="icon-fw icon-fire text-danger"></i> {{ t('Delete') }}</a>
-        {% endif %}
-      </u>
-    </li>
-    {% endif %}
+    <li id="page-management" class="nav-item dropdown"></li>
   {% endif %}
 </ul>
 

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

@@ -61,46 +61,7 @@
   </li>
 
   {% if !isTrashPage() %}
-    {% if page.isTopPage() %}
-    <li class="nav-item dropdown">
-      <a
-        {% if user %} role="button" class="nav-link dropdown-toggle dropdown-toggle-no-caret" data-toggle="dropdown" {% endif %}
-        {% if not user %}
-          class="nav-link dropdown-toggle dropdown-toggle-disabled dropdown-toggle-no-caret"
-          data-toggle="tooltip" data-placement="top" data-container="body" title="{{ t('Not available for guest') }}"
-        {% endif %}
-      >
-        <i class="icon-options-vertical"></i>
-      </a>
-      <div class="dropdown-menu dropdown-menu-right">
-        <a class="dropdown-item" href="#" data-target="#create-template" data-toggle="modal">
-          <i class="icon-fw icon-magic-wand"></i> {{ t('template.option_label.create/edit') }}
-        </a>
-      </div>
-    </li>
-    {% else %}
-    <li class="nav-item dropdown">
-      <a
-        {% if user %} role="button" class="nav-link dropdown-toggle dropdown-toggle-no-caret" data-toggle="dropdown" {% endif %}
-        {% if not user %}
-          class="nav-link dropdown-toggle dropdown-toggle-disabled dropdown-toggle-no-caret"
-          data-toggle="tooltip" data-placement="top" data-container="body" title="{{ t('Not available for guest') }}"
-        {% endif %}
-      >
-        <i class="icon-options-vertical"></i>
-      </a>
-      <div class="dropdown-menu dropdown-menu-right">
-        <a class="dropdown-item" href="#" data-target="#renamePage" data-toggle="modal"><i class="icon-fw icon-action-redo"></i> {{ t('Move/Rename') }}</a>
-        <a class="dropdown-item" href="#" data-target="#duplicatePage" data-toggle="modal"><i class="icon-fw icon-docs"></i> {{ t('Duplicate') }}</a>
-        <div class="dropdown-divider"></div>
-        <a class="dropdown-item" href="#" data-target="#create-template" data-toggle="modal"><i class="icon-fw icon-magic-wand"></i> {{ t('template.option_label.create/edit') }}</a>
-        {% if isDeletablePage() %}
-        <div class="dropdown-divider"></div>
-        <a class="dropdown-item" href="#" data-target="#deletePage" data-toggle="modal"><i class="icon-fw icon-fire text-danger"></i> {{ t('Delete') }}</a>
-        {% endif %}
-      </div>
-    </li>
-    {% endif %}
+    <li id="page-management" class="nav-item dropdown"></li>
   {% endif %}
 
 </ul>