yuken 4 лет назад
Родитель
Сommit
e0a868cd73

+ 9 - 21
packages/app/src/components/CustomNavigation/CustomNav.jsx

@@ -3,16 +3,18 @@ import React, {
 } from 'react';
 
 import PropTypes from 'prop-types';
-import { useTranslation } from 'react-i18next';
 import {
   Nav, NavItem, NavLink,
 } from 'reactstrap';
 
+<<<<<<< HEAD
 import { toastSuccess } from '~/client/util/apiNotification';
 import { useCurrentPagePath } from '~/stores/context';
 import { usePageDeleteModal } from '~/stores/modal';
 import { useSWRxDescendantsPageListForCurrrentPath, useSWRxPageInfoForList } from '~/stores/page';
 
+=======
+>>>>>>> feat/93162-create-empty-trash-in-trash-page
 
 function getBreakpointOneLevelLarger(breakpoint) {
   switch (breakpoint) {
@@ -89,16 +91,12 @@ CustomNavDropdown.propTypes = {
 
 
 export const CustomNavTab = (props) => {
-  const { t } = useTranslation();
   const navContainer = useRef();
   const [sliderWidth, setSliderWidth] = useState(0);
   const [sliderMarginLeft, setSliderMarginLeft] = useState(0);
-  const { open: openDeleteModal } = usePageDeleteModal();
-  const { data: currentPath } = useCurrentPagePath();
-  const { data: pagingResult, mutate } = useSWRxDescendantsPageListForCurrrentPath();
 
   const {
-    activeTab, navTabMapping, onNavSelected, hideBorderBottom, breakpointToHideInactiveTabsDown,
+    activeTab, navTabMapping, onNavSelected, hideBorderBottom, breakpointToHideInactiveTabsDown, navRightElement,
   } = props;
 
   const navTabRefs = useMemo(() => {
@@ -115,6 +113,7 @@ export const CustomNavTab = (props) => {
     }
   }, [onNavSelected]);
 
+<<<<<<< HEAD
   const pageIds = pagingResult?.items?.map(page => page._id);
   const { injectTo } = useSWRxPageInfoForList(pageIds, true, true);
 
@@ -139,6 +138,8 @@ export const CustomNavTab = (props) => {
     openDeleteModal(pageWithMetas, { onDeleted: onDeletedHandler, emptyTrash: true });
   };
 
+=======
+>>>>>>> feat/93162-create-empty-trash-in-trash-page
   function registerNavLink(key, elm) {
     if (elm != null) {
       navTabRefs[key] = elm;
@@ -182,9 +183,6 @@ export const CustomNavTab = (props) => {
     inactiveClassnames.push(`d-${breakpointOneLevelLarger}-block`);
   }
 
-  // trash page flag
-  const isTrash = currentPath === '/trash';
-
   return (
     <div className="grw-custom-nav-tab">
       <div ref={navContainer} className="d-flex justify-content-between">
@@ -207,18 +205,7 @@ export const CustomNavTab = (props) => {
             );
           })}
         </Nav>
-        { isTrash && (
-          <div className="d-flex align-items-center">
-            <button
-              type="button"
-              className="btn btn-outline-secondary rounded-pill text-danger d-flex align-items-center"
-              onClick={() => emptyTrashClickHandler()}
-            >
-              <i className="icon-fw icon-trash"></i>
-              <div>{t('modal_delete.empty_trash')}</div>
-            </button>
-          </div>
-        )}
+        {navRightElement}
       </div>
       <hr className="my-0 grw-nav-slide-hr border-none" style={{ width: `${sliderWidth}%`, marginLeft: `${sliderMarginLeft}%` }} />
       { !hideBorderBottom && <hr className="my-0 border-top-0 border-bottom" /> }
@@ -233,6 +220,7 @@ CustomNavTab.propTypes = {
   onNavSelected: PropTypes.func,
   hideBorderBottom: PropTypes.bool,
   breakpointToHideInactiveTabsDown: PropTypes.oneOf(['xs', 'sm', 'md', 'lg', 'xl']),
+  navRightElement: PropTypes.node,
 };
 
 CustomNavTab.defaultProps = {

+ 4 - 1
packages/app/src/components/CustomNavigation/CustomNavAndContents.jsx

@@ -1,4 +1,5 @@
 import React, { useState } from 'react';
+
 import PropTypes from 'prop-types';
 
 import CustomNav, { CustomNavTab, CustomNavDropdown } from './CustomNav';
@@ -7,7 +8,7 @@ import CustomTabContent from './CustomTabContent';
 
 const CustomNavAndContents = (props) => {
   const {
-    navTabMapping, defaultTabIndex, navigationMode, tabContentClasses, breakpointToHideInactiveTabsDown,
+    navTabMapping, defaultTabIndex, navigationMode, tabContentClasses, breakpointToHideInactiveTabsDown, navRightElement,
   } = props;
   const [activeTab, setActiveTab] = useState(Object.keys(props.navTabMapping)[defaultTabIndex || 0]);
 
@@ -31,6 +32,7 @@ const CustomNavAndContents = (props) => {
         navTabMapping={navTabMapping}
         onNavSelected={setActiveTab}
         breakpointToHideInactiveTabsDown={breakpointToHideInactiveTabsDown}
+        navRightElement={navRightElement}
       />
       <CustomTabContent activeTab={activeTab} navTabMapping={navTabMapping} additionalClassNames={tabContentClasses} />
     </>
@@ -43,6 +45,7 @@ CustomNavAndContents.propTypes = {
   navigationMode: PropTypes.oneOf(['both', 'tab', 'dropdown']),
   tabContentClasses: PropTypes.arrayOf(PropTypes.string),
   breakpointToHideInactiveTabsDown: PropTypes.oneOf(['xs', 'sm', 'md', 'lg', 'xl']),
+  navRightElement: PropTypes.node,
 };
 CustomNavAndContents.defaultProps = {
   navigationMode: 'tab',

+ 55 - 0
packages/app/src/components/EmptyTrashButton.tsx

@@ -0,0 +1,55 @@
+import React from 'react';
+
+import { useTranslation } from 'react-i18next';
+
+import {
+  IDataWithMeta,
+  IPageHasId,
+  IPageInfo,
+} from '~/interfaces/page';
+import { usePageDeleteModal } from '~/stores/modal';
+import { useSWRxDescendantsPageListForCurrrentPath, useSWRxPageInfoForList } from '~/stores/page';
+
+
+const EmptyTrashButton = () => {
+  const { t } = useTranslation();
+  const { open: openDeleteModal } = usePageDeleteModal();
+  const { data: pagingResult } = useSWRxDescendantsPageListForCurrrentPath();
+
+  const pageIds = pagingResult?.items?.map(page => page._id);
+  const { injectTo } = useSWRxPageInfoForList(pageIds, true, true);
+
+  let pageWithMetas: IDataWithMeta<IPageHasId, IPageInfo>[] = [];
+
+  const convertToIDataWithMeta = (page) => {
+    return { data: page };
+  };
+
+  if (pagingResult != null) {
+    const dataWithMetas = pagingResult.items.map(page => convertToIDataWithMeta(page));
+    pageWithMetas = injectTo(dataWithMetas);
+  }
+
+  const onDeletedHandler = (...args) => {
+    // process after multipe pages delete api
+  };
+
+  const emptyTrashClickHandler = () => {
+    openDeleteModal(pageWithMetas, { onDeleted: onDeletedHandler, emptyTrash: true });
+  };
+
+  return (
+    <div className="d-flex align-items-center">
+      <button
+        type="button"
+        className="btn btn-outline-secondary rounded-pill text-danger d-flex align-items-center"
+        onClick={() => emptyTrashClickHandler()}
+      >
+        <i className="icon-fw icon-trash"></i>
+        <div>{t('modal_delete.empty_trash')}</div>
+      </button>
+    </div>
+  );
+};
+
+export default EmptyTrashButton;

+ 5 - 3
packages/app/src/components/PageAttachment.jsx

@@ -1,14 +1,16 @@
 /* eslint-disable react/no-access-state-in-setstate */
 import React from 'react';
+
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 
-import PageAttachmentList from './PageAttachment/PageAttachmentList';
+import AppContainer from '~/client/services/AppContainer';
+import PageContainer from '~/client/services/PageContainer';
+
 import DeleteAttachmentModal from './PageAttachment/DeleteAttachmentModal';
+import PageAttachmentList from './PageAttachment/PageAttachmentList';
 import PaginationWrapper from './PaginationWrapper';
 import { withUnstatedContainers } from './UnstatedUtils';
-import AppContainer from '~/client/services/AppContainer';
-import PageContainer from '~/client/services/PageContainer';
 
 class PageAttachment extends React.Component {
 

+ 24 - 10
packages/app/src/components/PageDeleteModal.tsx

@@ -1,4 +1,6 @@
-import React, { useState, FC, useMemo } from 'react';
+import React, {
+  useState, FC, useMemo, useCallback,
+} from 'react';
 
 import { useTranslation } from 'react-i18next';
 import {
@@ -216,11 +218,9 @@ const PageDeleteModal: FC = () => {
     );
   }
 
-  const renderCompletelyDeleteAlert = () => {
-    return (
-      <p className="form-text mt-0">{t('modal_delete.empty_trash_alert')}</p>
-    );
-  };
+  const renderCompletelyDeleteAlert = useMemo(() => {
+    return <p className="form-text mt-0">{t('modal_delete.empty_trash_alert')}</p>;
+  }, [t]);
 
   const renderPagePathsToDelete = () => {
     const pages = injectedPages != null && injectedPages.length > 0 ? injectedPages : deleteModalData?.pages;
@@ -236,6 +236,23 @@ const PageDeleteModal: FC = () => {
     return <></>;
   };
 
+  const renderDeleteModalOptions = useCallback(() => {
+    if (emptyTrash) {
+      return renderCompletelyDeleteAlert;
+    }
+
+    if (!isDeletable) {
+      return;
+    }
+
+    return (
+      <>
+        {renderDeleteRecursivelyForm()}
+        {!forceDeleteCompletelyMode && renderDeleteCompletelyForm()}
+      </>
+    );
+  }, [t, deleteModalData, isDeleteCompletely, isDeleteRecursively]);
+
   return (
     <Modal size="lg" isOpen={isOpened} toggle={closeDeleteModal} data-testid="page-delete-modal" className="grw-create-page">
       <ModalHeader tag="h4" toggle={closeDeleteModal} className={`bg-${deleteIconAndKey[deleteMode].color} text-light`}>
@@ -246,12 +263,9 @@ const PageDeleteModal: FC = () => {
         <div className="form-group grw-scrollable-modal-body pb-1">
           <label>{ t('modal_delete.deleting_page') }:</label><br />
           {/* Todo: change the way to show path on modal when too many pages are selected */}
-          {/* https://redmine.weseek.co.jp/issues/82787 */}
           {renderPagePathsToDelete()}
         </div>
-        { isDeletable && !emptyTrash && renderDeleteRecursivelyForm()}
-        { isDeletable && !forceDeleteCompletelyMode && !emptyTrash && renderDeleteCompletelyForm() }
-        { emptyTrash && renderCompletelyDeleteAlert() }
+        {renderDeleteModalOptions()}
       </ModalBody>
       <ModalFooter>
         <ApiErrorMessageList errs={errs} />

+ 9 - 2
packages/app/src/components/TrashPageList.jsx

@@ -1,9 +1,12 @@
 import React, { useMemo } from 'react';
+
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
-import PageListIcon from './Icons/PageListIcon';
+
 import CustomNavAndContents from './CustomNavigation/CustomNavAndContents';
 import { DescendantsPageListForCurrentPath } from './DescendantsPageList';
+import EmptyTrashButton from './EmptyTrashButton';
+import PageListIcon from './Icons/PageListIcon';
 
 
 const TrashPageList = (props) => {
@@ -20,9 +23,13 @@ const TrashPageList = (props) => {
     };
   }, [t]);
 
+  const emptyTrashButton = useMemo(() => {
+    return <EmptyTrashButton />;
+  }, [t]);
+
   return (
     <div data-testid="trash-page-list" className="mt-5 d-edit-none">
-      <CustomNavAndContents navTabMapping={navTabMapping} />
+      <CustomNavAndContents navTabMapping={navTabMapping} navRightElement={emptyTrashButton} />
     </div>
   );
 };

+ 4 - 3
packages/app/src/server/routes/apiv3/attachment.js

@@ -8,8 +8,8 @@ const express = require('express');
 
 const router = express.Router();
 const { query } = require('express-validator');
-const { serializeUserSecurely } = require('../../models/serializers/user-serializer');
 
+const { serializeUserSecurely } = require('../../models/serializers/user-serializer');
 const ErrorV3 = require('../../models/vo/error-apiv3');
 
 /**
@@ -28,7 +28,8 @@ module.exports = (crowi) => {
   const validator = {
     retrieveAttachments: [
       query('pageId').isMongoId().withMessage('pageId is required'),
-      query('limit').if(value => value != null).isInt({ max: 100 }).withMessage('You should set less than 100 or not to set limit.'),
+      query('page').optional().isInt().withMessage('page must be a number'),
+      query('limit').optional().isInt({ max: 100 }).withMessage('You should set less than 100 or not to set limit.'),
     ],
   };
   /**
@@ -52,7 +53,7 @@ module.exports = (crowi) => {
   router.get('/list', accessTokenParser, loginRequired, validator.retrieveAttachments, apiV3FormValidator, async(req, res) => {
 
     const limit = req.query.limit || await crowi.configManager.getConfig('crowi', 'customize:showPageLimitationS') || 10;
-    const page = req.query.page;
+    const page = req.query.page || 1;
     const offset = (page - 1) * limit;
 
     try {

+ 19 - 7
packages/app/src/styles/theme/_apply-colors-dark.scss

@@ -277,19 +277,30 @@ ul.pagination {
   // Pagetree
   .grw-pagetree {
     @include override-list-group-item-for-pagetree(
-      $color-list,
+      $gray-200,
       $bgcolor-sidebar-list-group,
-      $color-list-hover,
-      $bgcolor-list-hover,
-      $color-list-active,
-      lighten($bgcolor-list-hover, 5%)
+      $gray-200,
+      lighten($bgcolor-sidebar-context, 8%),
+      $gray-200,
+      lighten($bgcolor-sidebar-context, 15%)
     );
     .grw-pagetree-triangle-btn {
       @include button-outline-svg-icon-variant($secondary, $gray-200);
     }
     .grw-pagetree-count {
       color: $gray-400;
-      background: $gray-700;
+      background: lighten($bgcolor-sidebar-context, 15%);
+    }
+    .btn-page-item-control {
+      @include button-outline-variant($gray-500, $gray-500, $secondary, transparent);
+      @include hover() {
+        background-color: lighten($bgcolor-sidebar-context, 20%);
+      }
+      &:not(:disabled):not(.disabled):active,
+      &:not(:disabled):not(.disabled).active {
+        background-color: lighten($bgcolor-sidebar-context, 34%);
+      }
+      box-shadow: none !important;
     }
   }
   .private-legacy-pages-link {
@@ -302,11 +313,12 @@ ul.pagination {
 .btn.btn-page-item-control {
   @include button-outline-variant($gray-500, $gray-500, $secondary, transparent);
   @include hover() {
-    background-color: $gray-600;
+    background-color: $gray-700;
   }
   &:not(:disabled):not(.disabled):active,
   &:not(:disabled):not(.disabled).active {
     color: $gray-200;
+    background-color: $gray-600;
   }
   box-shadow: none !important;
 }