Explorar el Código

Merge branch 'support/reactify-pagemanagement-modal-for-merge' into support/reactify-duplicate-page-modal

itizawa hace 5 años
padre
commit
f4fe7c1687

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

@@ -285,7 +285,8 @@
     "completely": "Delete completely instead of putting it into trash."
     "completely": "Delete completely instead of putting it into trash."
   },
   },
   "modal_empty":{
   "modal_empty":{
-    "empty_the_trash": "Empty The Trash"
+    "empty_the_trash": "Empty The Trash",
+    "notice": "The pages deleted completely are unrecoverable."
   },
   },
   "modal_duplicate": {
   "modal_duplicate": {
     "label": {
     "label": {

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

@@ -283,7 +283,8 @@
     "completely": "ゴミ箱を経由せず、完全に削除します"
     "completely": "ゴミ箱を経由せず、完全に削除します"
   },
   },
   "modal_empty":{
   "modal_empty":{
-    "empty_the_trash": "ゴミ箱を空にする"
+    "empty_the_trash": "ゴミ箱を空にする",
+    "notice": "完全削除したページは元に戻すことができません"
   },
   },
   "modal_duplicate": {
   "modal_duplicate": {
     "label": {
     "label": {
@@ -330,7 +331,7 @@
     "activate_user_success": "{{username}}を有効化しました",
     "activate_user_success": "{{username}}を有効化しました",
     "deactivate_user_success": "{{username}}を無効化しました",
     "deactivate_user_success": "{{username}}を無効化しました",
     "remove_user_success": "{{username}}を削除しました",
     "remove_user_success": "{{username}}を削除しました",
-    "remove_external_user_success": "{{accountId}}を削除しました "
+    "remove_external_user_success": "{{accountId}}を削除しました"
   },
   },
   "template": {
   "template": {
     "modal_label": {
     "modal_label": {

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

@@ -23,6 +23,7 @@ import PageTimeline from './components/PageTimeline';
 import CommentEditorLazyRenderer from './components/PageComment/CommentEditorLazyRenderer';
 import CommentEditorLazyRenderer from './components/PageComment/CommentEditorLazyRenderer';
 import PageManagement from './components/Page/PageManagement';
 import PageManagement from './components/Page/PageManagement';
 import PageDuplicateModal from './components/PageDuplicateModal';
 import PageDuplicateModal from './components/PageDuplicateModal';
+import TrashPageAlert from './components/Page/TrashPageAlert';
 import PageAttachment from './components/PageAttachment';
 import PageAttachment from './components/PageAttachment';
 import PageStatusAlert from './components/PageStatusAlert';
 import PageStatusAlert from './components/PageStatusAlert';
 import PagePathAutoComplete from './components/PagePathAutoComplete';
 import PagePathAutoComplete from './components/PagePathAutoComplete';
@@ -78,6 +79,8 @@ Object.assign(componentMappings, {
   'page-status-alert': <PageStatusAlert />,
   'page-status-alert': <PageStatusAlert />,
   'save-page-controls': <SavePageControls />,
   'save-page-controls': <SavePageControls />,
 
 
+  'trash-page-alert': <TrashPageAlert />,
+
   'page-timeline': <PageTimeline />,
   'page-timeline': <PageTimeline />,
 
 
   'personal-setting': <PersonalSettings crowi={personalContainer} />,
   'personal-setting': <PersonalSettings crowi={personalContainer} />,

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

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

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

@@ -0,0 +1,107 @@
+import React, { useState } from 'react';
+import PropTypes from 'prop-types';
+
+import { withTranslation } from 'react-i18next';
+import { toastError } from '../../util/apiNotification';
+
+import { createSubscribedElement } from '../UnstatedUtils';
+import AppContainer from '../../services/AppContainer';
+import PageContainer from '../../services/PageContainer';
+import UserPicture from '../User/UserPicture';
+import EmptyTrashModal from '../EmptyTrashModal';
+
+
+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);
+
+  function openEmptyTrashModal() {
+    setIsEmptyTrashModalShown(true);
+  }
+
+  function closeEmptyTrashModal() {
+    setIsEmptyTrashModalShown(false);
+  }
+
+  async function onClickDeleteBtn() {
+    try {
+      await appContainer.apiv3Delete('/pages/empty-trash');
+      window.location.reload();
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }
+
+  function renderEmptyButton() {
+    return (
+      <button
+        href="#"
+        type="button"
+        className="btn btn-danger rounded-pill btn-sm ml-auto"
+        data-target="#emptyTrash"
+        onClick={openEmptyTrashModal}
+      >
+        <i className="icon-trash" aria-hidden="true"></i>{ t('modal_empty.empty_the_trash') }
+      </button>
+    );
+  }
+
+  function renderTrashPageManagementButtons() {
+    return (
+      <>
+        <button
+          type="button"
+          className="btn btn-outline-secondary rounded-pill btn-sm ml-auto mr-2"
+          data-target="#putBackPage"
+          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}
+          data-target="#deletePage"
+          data-toggle="modal"
+        >
+          <i className="icon-fire" aria-hidden="true"></i> { t('Delete Completely') }
+        </button>
+      </>
+    );
+  }
+
+  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>
+      <EmptyTrashModal isOpen={isEmptyTrashModalShown} toggle={closeEmptyTrashModal} onClickSubmit={onClickDeleteBtn} />
+    </>
+  );
+};
+
+/**
+ * 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);

+ 3 - 2
src/client/js/services/PageContainer.js

@@ -32,7 +32,6 @@ export default class PageContainer extends Container {
 
 
     const revisionId = mainContent.getAttribute('data-page-revision-id');
     const revisionId = mainContent.getAttribute('data-page-revision-id');
     const path = decodeURI(mainContent.getAttribute('data-path'));
     const path = decodeURI(mainContent.getAttribute('data-path'));
-
     this.state = {
     this.state = {
       // local page data
       // local page data
       markdown: null, // will be initialized after initStateMarkdown()
       markdown: null, // will be initialized after initStateMarkdown()
@@ -48,8 +47,10 @@ export default class PageContainer extends Container {
       createdAt: mainContent.getAttribute('data-page-created-at'),
       createdAt: mainContent.getAttribute('data-page-created-at'),
       creator: JSON.parse(mainContent.getAttribute('data-page-creator')),
       creator: JSON.parse(mainContent.getAttribute('data-page-creator')),
       updatedAt: mainContent.getAttribute('data-page-updated-at'),
       updatedAt: mainContent.getAttribute('data-page-updated-at'),
-
+      isDeleted:  JSON.parse(mainContent.getAttribute('data-page-is-deleted')),
+      isAbleToDeleteCompletely:  JSON.parse(mainContent.getAttribute('data-page-is-able-to-delete-completely')),
       tags: [],
       tags: [],
+      hasChildren: JSON.parse(mainContent.getAttribute('data-page-has-children')),
       templateTagData: mainContent.getAttribute('data-template-tags') || null,
       templateTagData: mainContent.getAttribute('data-template-tags') || null,
 
 
       // latest(on remote) information
       // latest(on remote) information

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

@@ -107,13 +107,7 @@
       {% endif %}
       {% endif %}
 
 
       {% if user %}
       {% 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>
       <li class="nav-item d-none d-md-block">
       <li class="nav-item d-none d-md-block">
         <a class="nav-link" href="https://docs.growi.org/" target="_blank">
         <a class="nav-link" href="https://docs.growi.org/" target="_blank">
           <i class="icon-question mr-2"></i><span class="mr-2">{{ t('Help') }}</span><span class="small"><i class="icon-share-alt"></i></span>
           <i class="icon-question mr-2"></i><span class="mr-2">{{ t('Help') }}</span><span class="small"><i class="icon-share-alt"></i></span>
@@ -149,17 +143,14 @@
 
 
 <div class="grw-fixed-controls-container d-md-none d-edit-none animated fadeInUp faster">
 <div class="grw-fixed-controls-container d-md-none d-edit-none animated fadeInUp faster">
   <div class="grw-fixed-controls-button-container rounded-circle">
   <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>
 </div>
 </div>
 
 
 <!-- /#staff-credit -->
 <!-- /#staff-credit -->
 <div id="staff-credit"></div>
 <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' %}
 {% include '../modal/shortcuts.html' %}
 
 
 {% block body_end %}
 {% block body_end %}

+ 1 - 19
src/server/views/widget/page_alerts.html

@@ -82,25 +82,7 @@
     {% endif %}
     {% endif %}
 
 
     {% if isTrashPage() %}
     {% 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 %}
     {% endif %}
   </div>
   </div>
 </div>
 </div>

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

@@ -11,16 +11,21 @@
   data-page-has-draft-on-hackmd="{% if hasDraftOnHackmd %}{{ hasDraftOnHackmd.toString() }}{% endif %}"
   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-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-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-able-to-delete-completely="{% if user.canDeleteCompletely(page.creator._id) %}true{% else %}false{% endif %}"
   data-slack-channels="{{ slack|default('') }}"
   data-slack-channels="{{ slack|default('') }}"
   data-page-created-at="{% if page %}{{ page.createdAt|datetz('Y/m/d H:i:s') }}{% endif %}"
   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-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-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 %}
 {% else %}
 <div id="content-main" class="content-main"
 <div id="content-main" class="content-main"
   data-path="{{ encodeURI(path) }}"
   data-path="{{ encodeURI(path) }}"
   data-current-user="{% if user %}{{ user._id.toString() }}{% endif %}"
   data-current-user="{% if user %}{{ user._id.toString() }}{% endif %}"
   data-slack-channels="{{ slack|default('') }}"
   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 %}
 {% endif %}