Просмотр исходного кода

Merge branch 'support/use-turborepo' into support/build-remark-lsx-with-vite

Yuki Takei 3 лет назад
Родитель
Сommit
9338eb5dac

+ 74 - 0
apps/app/src/components/InfiniteScroll.tsx

@@ -0,0 +1,74 @@
+import React, {
+  Ref, useEffect, useState,
+} from 'react';
+
+import type { SWRInfiniteResponse } from 'swr/infinite';
+
+type Props<T> = {
+  swrInifiniteResponse: SWRInfiniteResponse<T>
+  children: React.ReactNode,
+  loadingIndicator?: React.ReactNode
+  endingIndicator?: React.ReactNode
+  isReachingEnd?: boolean
+  offset?: number
+}
+
+const useIntersection = <E extends HTMLElement>(): [boolean, Ref<E>] => {
+  const [intersecting, setIntersecting] = useState<boolean>(false);
+  const [element, setElement] = useState<HTMLElement>();
+  useEffect(() => {
+    if (element != null) {
+      const observer = new IntersectionObserver((entries) => {
+        setIntersecting(entries[0]?.isIntersecting);
+      });
+      observer.observe(element);
+      return () => observer.unobserve(element);
+    }
+    return;
+  }, [element]);
+  return [intersecting, el => el && setElement(el)];
+};
+
+const LoadingIndicator = (): React.ReactElement => {
+  return (
+    <div className="text-muted text-center">
+      <i className="fa fa-2x fa-spinner fa-pulse mr-1"></i>
+    </div>
+  );
+};
+
+const InfiniteScroll = <E, >(props: Props<E>): React.ReactElement<Props<E>> => {
+  const {
+    swrInifiniteResponse: {
+      setSize, isValidating,
+    },
+    children,
+    loadingIndicator,
+    endingIndicator,
+    isReachingEnd,
+    offset = 0,
+  } = props;
+
+  const [intersecting, ref] = useIntersection<HTMLDivElement>();
+
+  useEffect(() => {
+    if (intersecting && !isValidating && !isReachingEnd) {
+      setSize(size => size + 1);
+    }
+  }, [setSize, intersecting, isValidating, isReachingEnd]);
+
+  return (
+    <>
+      {children}
+      <div style={{ position: 'relative' }}>
+        <div ref={ref} style={{ position: 'absolute', top: offset }}></div>
+        {isReachingEnd
+          ? endingIndicator
+          : loadingIndicator || <LoadingIndicator />
+        }
+      </div>
+    </>
+  );
+};
+
+export default InfiniteScroll;

+ 28 - 3
apps/app/src/components/PageAccessoriesModal.tsx

@@ -17,7 +17,7 @@ import AttachmentIcon from './Icons/AttachmentIcon';
 import HistoryIcon from './Icons/HistoryIcon';
 import HistoryIcon from './Icons/HistoryIcon';
 import ShareLinkIcon from './Icons/ShareLinkIcon';
 import ShareLinkIcon from './Icons/ShareLinkIcon';
 import PageAttachment from './PageAttachment';
 import PageAttachment from './PageAttachment';
-import { PageHistory } from './PageHistory';
+import { PageHistory, getQueryParam } from './PageHistory';
 import ShareLink from './ShareLink/ShareLink';
 import ShareLink from './ShareLink/ShareLink';
 
 
 import styles from './PageAccessoriesModal.module.scss';
 import styles from './PageAccessoriesModal.module.scss';
@@ -27,6 +27,9 @@ const PageAccessoriesModal = (): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
 
 
   const [activeTab, setActiveTab] = useState<PageAccessoriesModalContents>(PageAccessoriesModalContents.PageHistory);
   const [activeTab, setActiveTab] = useState<PageAccessoriesModalContents>(PageAccessoriesModalContents.PageHistory);
+  const [sourceRevisionId, setSourceRevisionId] = useState<string>();
+  const [targetRevisionId, setTargetRevisionId] = useState<string>();
+
   const [isWindowExpanded, setIsWindowExpanded] = useState(false);
   const [isWindowExpanded, setIsWindowExpanded] = useState(false);
 
 
   const { data: isSharedUser } = useIsSharedUser();
   const { data: isSharedUser } = useIsSharedUser();
@@ -48,6 +51,28 @@ const PageAccessoriesModal = (): JSX.Element => {
     }, false);
     }, false);
   }, [mutate, status]);
   }, [mutate, status]);
 
 
+  // Set sourceRevisionId and targetRevisionId as state with valid object id string
+  useEffect(() => {
+    const queryParams = getQueryParam();
+    // https://regex101.com/r/YHTDsr/1
+    const regex = /^([0-9a-f]{24})...([0-9a-f]{24})$/i;
+
+    if (queryParams == null || !regex.test(queryParams)) {
+      return;
+    }
+
+    const matches = queryParams.match(regex);
+
+    if (matches == null) {
+      return;
+    }
+
+    const [, sourceRevisionId, targetRevisionId] = matches;
+    setSourceRevisionId(sourceRevisionId);
+    setTargetRevisionId(targetRevisionId);
+    mutate({ isOpened: true });
+  }, [mutate]);
+
   const navTabMapping = useMemo(() => {
   const navTabMapping = useMemo(() => {
     const isOpened = status == null ? false : status.isOpened;
     const isOpened = status == null ? false : status.isOpened;
     return {
     return {
@@ -57,7 +82,7 @@ const PageAccessoriesModal = (): JSX.Element => {
           if (!isOpened) {
           if (!isOpened) {
             return <></>;
             return <></>;
           }
           }
-          return <PageHistory onClose={close}/>;
+          return <PageHistory onClose={close} sourceRevisionId={sourceRevisionId} targetRevisionId={targetRevisionId}/>;
         },
         },
         i18n: t('History'),
         i18n: t('History'),
         index: 0,
         index: 0,
@@ -87,7 +112,7 @@ const PageAccessoriesModal = (): JSX.Element => {
         isLinkEnabled: () => !isGuestUser && !isSharedUser && !isLinkSharingDisabled,
         isLinkEnabled: () => !isGuestUser && !isSharedUser && !isLinkSharingDisabled,
       },
       },
     };
     };
-  }, [status, t, close, isGuestUser, isSharedUser, isLinkSharingDisabled]);
+  }, [status, t, close, sourceRevisionId, targetRevisionId, isGuestUser, isSharedUser, isLinkSharingDisabled]);
 
 
   const buttons = useMemo(() => (
   const buttons = useMemo(() => (
     <div className="d-flex flex-nowrap">
     <div className="d-flex flex-nowrap">

+ 1 - 1
apps/app/src/components/PageDeleteModal.tsx

@@ -229,7 +229,7 @@ const PageDeleteModal: FC = () => {
       return pages.map(page => (
       return pages.map(page => (
         <p key={page.data._id} className="mb-1">
         <p key={page.data._id} className="mb-1">
           <code>{ page.data.path }</code>
           <code>{ page.data.path }</code>
-          { !page.meta?.isDeletable && <span className="ml-3 text-danger"><strong>(CAN NOT TO DELETE)</strong></span> }
+          { page.meta?.isDeletable != null && !page.meta.isDeletable && <span className="ml-3 text-danger"><strong>(CAN NOT TO DELETE)</strong></span> }
         </p>
         </p>
       ));
       ));
     }
     }

+ 3 - 0
apps/app/src/components/PageEditor.tsx

@@ -310,6 +310,9 @@ const PageEditor = React.memo((): JSX.Element => {
       if (pageId != null) {
       if (pageId != null) {
         formData.append('page_id', pageId);
         formData.append('page_id', pageId);
       }
       }
+      if (pageId == null && markdownToSave.current != null) {
+        formData.append('page_body', markdownToSave.current);
+      }
 
 
       res = await apiPostForm('/attachments.add', formData);
       res = await apiPostForm('/attachments.add', formData);
       const attachment = res.attachment;
       const attachment = res.attachment;

+ 24 - 67
apps/app/src/components/PageHistory.tsx

@@ -1,85 +1,42 @@
-import React, { useState, useEffect } from 'react';
-
-import { IRevisionHasPageId } from '@growi/core';
+import React from 'react';
 
 
 import { useCurrentPageId } from '~/stores/context';
 import { useCurrentPageId } from '~/stores/context';
-import { useSWRxPageRevisions, useCurrentPagePath } from '~/stores/page';
+import { useCurrentPagePath } from '~/stores/page';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import { PageRevisionTable } from './PageHistory/PageRevisionTable';
 import { PageRevisionTable } from './PageHistory/PageRevisionTable';
-import PaginationWrapper from './PaginationWrapper';
-import { RevisionComparer } from './RevisionComparer/RevisionComparer';
 
 
 const logger = loggerFactory('growi:PageHistory');
 const logger = loggerFactory('growi:PageHistory');
 
 
-export const PageHistory: React.FC<{ onClose: () => void }> = ({ onClose }) => {
+type PageHistoryProps = {
+  sourceRevisionId?: string,
+  targetRevisionId?: string
+  onClose: () => void
+}
+
+// Get string from 'compare' query params
+export const getQueryParam = (): string | null => {
+  const query: URLSearchParams = new URL(window.location.href).searchParams;
+  return query.get('compare');
+};
 
 
-  const [activePage, setActivePage] = useState(1);
+export const PageHistory: React.FC<PageHistoryProps> = (props: PageHistoryProps) => {
+  const { sourceRevisionId, targetRevisionId, onClose } = props;
 
 
   const { data: currentPageId } = useCurrentPageId();
   const { data: currentPageId } = useCurrentPageId();
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: currentPagePath } = useCurrentPagePath();
 
 
-  const { data: revisionsData, mutate: mutatePageRevisions } = useSWRxPageRevisions(activePage, 10, currentPageId);
-
-  const [sourceRevision, setSourceRevision] = useState<IRevisionHasPageId>();
-  const [targetRevision, setTargetRevision] = useState<IRevisionHasPageId>();
-
-  useEffect(() => {
-    if (revisionsData != null) {
-      setSourceRevision(revisionsData.revisions[0]);
-      setTargetRevision(revisionsData.revisions[0]);
-    }
-  }, [revisionsData]);
-
-  useEffect(() => {
-    mutatePageRevisions();
-  });
-
-  const pagingLimit = 10;
-
-  if (revisionsData == null || sourceRevision == null || targetRevision == null || currentPageId == null || currentPagePath == null) {
-    return (
-      <div className="text-muted text-center">
-        <i className="fa fa-2x fa-spinner fa-pulse mt-3"></i>
-      </div>
-    );
-  }
-
-  const pager = () => {
-    return (
-      <PaginationWrapper
-        activePage={activePage}
-        changePage={setActivePage}
-        totalItemsCount={revisionsData.totalCounts}
-        pagingLimit={pagingLimit}
-        align="center"
-      />
-    );
-  };
-
   return (
   return (
     <div className="revision-history" data-testid="page-history">
     <div className="revision-history" data-testid="page-history">
-      <PageRevisionTable
-        revisions={revisionsData.revisions}
-        pagingLimit={pagingLimit}
-        sourceRevision={sourceRevision}
-        targetRevision={targetRevision}
-        currentPageId={currentPageId}
-        currentPagePath={currentPagePath}
-        onChangeSourceInvoked={setSourceRevision}
-        onChangeTargetInvoked={setTargetRevision}
-        onClose={onClose}
-      />
-      <div className="my-3">
-        {pager()}
-      </div>
-      <RevisionComparer
-        sourceRevision={sourceRevision}
-        targetRevision={targetRevision}
-        currentPageId={currentPageId}
-        currentPagePath={currentPagePath}
-        onClose={onClose}
-      />
+      {currentPageId != null && currentPagePath != null && (
+        <PageRevisionTable
+          sourceRevisionId={sourceRevisionId}
+          targetRevisionId={targetRevisionId}
+          currentPageId={currentPageId}
+          currentPagePath={currentPagePath}
+          onClose={onClose}
+        />
+      )}
     </div>
     </div>
   );
   );
 };
 };

+ 128 - 49
apps/app/src/components/PageHistory/PageRevisionTable.tsx

@@ -1,37 +1,110 @@
-import React from 'react';
+import React, {
+  useEffect, useRef, useState,
+} from 'react';
 
 
-import { IRevisionHasId } from '@growi/core';
+import { IRevisionHasId, IRevisionHasPageId } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
+import { useSWRxInfinitePageRevisions } from '~/stores/page';
+
+import { RevisionComparer } from '../RevisionComparer/RevisionComparer';
+
 import { Revision } from './Revision';
 import { Revision } from './Revision';
 
 
 import styles from './PageRevisionTable.module.scss';
 import styles from './PageRevisionTable.module.scss';
 
 
-type PageRevisionTAble = {
-  revisions: IRevisionHasId[],
-  pagingLimit: number,
-  sourceRevision: IRevisionHasId,
-  targetRevision: IRevisionHasId,
-  currentPageId: string,
-  currentPagePath: string,
-  onChangeSourceInvoked: React.Dispatch<React.SetStateAction<IRevisionHasId | undefined>>,
-  onChangeTargetInvoked: React.Dispatch<React.SetStateAction<IRevisionHasId | undefined>>,
+type PageRevisionTableProps = {
+  sourceRevisionId?: string
+  targetRevisionId?: string
   onClose: () => void,
   onClose: () => void,
+  currentPageId: string
+  currentPagePath: string
 }
 }
 
 
-export const PageRevisionTable = (props: PageRevisionTAble): JSX.Element => {
+export const PageRevisionTable = (props: PageRevisionTableProps): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
 
 
+  const REVISIONS_PER_PAGE = 10;
+
   const {
   const {
-    revisions, pagingLimit, sourceRevision, targetRevision, currentPageId, currentPagePath,
-    onChangeSourceInvoked, onChangeTargetInvoked, onClose,
+    sourceRevisionId, targetRevisionId, onClose, currentPageId, currentPagePath,
   } = props;
   } = props;
 
 
-  const revisionCount = revisions.length;
-  const latestRevision = revisions[0];
-  const oldestRevision = revisions[revisions.length - 1];
+  // Load all data if source revision id and target revision id not null
+  const revisionPerPage = (sourceRevisionId != null && targetRevisionId != null) ? 0 : REVISIONS_PER_PAGE;
+  const swrInifiniteResponse = useSWRxInfinitePageRevisions(currentPageId, revisionPerPage);
+
 
 
-  const renderRow = (revision: IRevisionHasId, previousRevision: IRevisionHasId, latestRevision: IRevisionHasId,
+  const {
+    data, size, error, setSize, isValidating,
+  } = swrInifiniteResponse;
+
+  const revisions = data && data[0].revisions;
+  const oldestRevision = revisions && revisions[revisions.length - 1];
+
+  // First load
+  const isLoadingInitialData = !data && !error;
+  const isLoadingMore = isLoadingInitialData
+    || (isValidating && data != null && typeof data[size - 1] === 'undefined');
+  const isReachingEnd = (revisionPerPage === 0) || !!(data != null && data[data.length - 1]?.revisions.length < REVISIONS_PER_PAGE);
+
+  const [sourceRevision, setSourceRevision] = useState<IRevisionHasPageId>();
+  const [targetRevision, setTargetRevision] = useState<IRevisionHasPageId>();
+
+  const tbodyRef = useRef<HTMLTableSectionElement>(null);
+
+
+  useEffect(() => {
+    if (revisions != null) {
+      if (sourceRevisionId != null && targetRevisionId != null) {
+        const sourceRevision = revisions.filter(revision => revision._id === sourceRevisionId)[0];
+        const targetRevision = revisions.filter(revision => revision._id === targetRevisionId)[0];
+        setSourceRevision(sourceRevision);
+        setTargetRevision(targetRevision);
+      }
+      else {
+        const latestRevision = revisions != null ? revisions[0] : null;
+        if (latestRevision != null) {
+          setSourceRevision(latestRevision);
+          setTargetRevision(latestRevision);
+        }
+      }
+    }
+  }, [revisions, sourceRevisionId, targetRevisionId]);
+
+  useEffect(() => {
+    // Apply ref to tbody
+    const tbody = tbodyRef.current;
+    const handleScroll = () => {
+      const offset = 30; // Threshold before scroll actually reaching the end
+      if (tbody) {
+        // Scroll end
+        const isEnd = tbody.scrollTop + tbody.clientHeight + offset >= tbody.scrollHeight;
+        if (isEnd && !isLoadingMore && !isReachingEnd) {
+          setSize(size + 1);
+        }
+      }
+    };
+    if (tbody) {
+      tbody.addEventListener('scroll', handleScroll);
+    }
+    return () => {
+      if (tbody) {
+        tbody.removeEventListener('scroll', handleScroll);
+      }
+    };
+  }, [isLoadingMore, isReachingEnd, setSize, size]);
+
+
+  const onChangeSourceInvoked: React.Dispatch<React.SetStateAction<IRevisionHasId | undefined>> = (revision: IRevisionHasPageId) => {
+    setSourceRevision(revision);
+  };
+  const onChangeTargetInvoked: React.Dispatch<React.SetStateAction<IRevisionHasId | undefined>> = (revision: IRevisionHasPageId) => {
+    setTargetRevision(revision);
+  };
+
+
+  const renderRow = (revision: IRevisionHasPageId, previousRevision: IRevisionHasPageId, latestRevision: IRevisionHasPageId,
       isOldestRevision: boolean, hasDiff: boolean) => {
       isOldestRevision: boolean, hasDiff: boolean) => {
 
 
     const revisionId = revision._id;
     const revisionId = revision._id;
@@ -52,10 +125,10 @@ export const PageRevisionTable = (props: PageRevisionTAble): JSX.Element => {
           <div className="d-lg-flex">
           <div className="d-lg-flex">
             <Revision
             <Revision
               revision={revision}
               revision={revision}
-              currentPageId={currentPageId}
-              currentPagePath={currentPagePath}
               isLatestRevision={revision === latestRevision}
               isLatestRevision={revision === latestRevision}
               hasDiff={hasDiff}
               hasDiff={hasDiff}
+              currentPageId={currentPageId}
+              currentPagePath={currentPagePath}
               key={`revision-history-rev-${revisionId}`}
               key={`revision-history-rev-${revisionId}`}
               onClose={onClose}
               onClose={onClose}
             />
             />
@@ -118,36 +191,42 @@ export const PageRevisionTable = (props: PageRevisionTAble): JSX.Element => {
     );
     );
   };
   };
 
 
-  const revisionList = revisions.map((revision, idx) => {
-    // Returns null because the last revision is for the bottom diff display
-    if (idx === pagingLimit) {
-      return null;
-    }
-
-    // if it is the first revision, show full text as diff text
-    const previousRevision = (idx + 1 < revisionCount) ? revisions[idx + 1] : revision;
-
-    const isOldestRevision = revision === oldestRevision;
-
-    // set 'true' if undefined for backward compatibility
-    const hasDiff = revision.hasDiffToPrev !== false;
-
-    return renderRow(revision, previousRevision, latestRevision, isOldestRevision, hasDiff);
-  });
-
   return (
   return (
-    <table className={`${styles['revision-history-table']} table revision-history-table`}>
-      <thead>
-        <tr className="d-flex">
-          <th className="col">{t('page_history.revision')}</th>
-          <th className="col-1">{t('page_history.comparing_source')}</th>
-          <th className="col-2">{t('page_history.comparing_target')}</th>
-        </tr>
-      </thead>
-      <tbody className="overflow-auto d-block">
-        {revisionList}
-      </tbody>
-    </table>
+    <>
+      <table className={`${styles['revision-history-table']} table revision-history-table`}>
+        <thead>
+          <tr className="d-flex">
+            <th className="col">{t('page_history.revision')}</th>
+            <th className="col-1">{t('page_history.comparing_source')}</th>
+            <th className="col-2">{t('page_history.comparing_target')}</th>
+          </tr>
+        </thead>
+        <tbody className="overflow-auto d-block" ref={tbodyRef}>
+          {revisions != null && data != null && data.map(apiResult => apiResult.revisions).flat()
+            .map((revision, idx) => {
+              const previousRevision = (idx + 1 < revisions?.length) ? revisions[idx + 1] : revision;
+
+              const isOldestRevision = revision === oldestRevision;
+              const latestRevision = revisions[0];
+
+              // set 'true' if undefined for backward compatibility
+              const hasDiff = revision.hasDiffToPrev !== false;
+              return renderRow(revision, previousRevision, latestRevision, isOldestRevision, hasDiff);
+            })
+          }
+        </tbody>
+      </table>
+
+      {sourceRevision != null && targetRevision != null && (
+        <RevisionComparer
+          sourceRevision={sourceRevision}
+          targetRevision={targetRevision}
+          currentPageId={currentPageId}
+          currentPagePath={currentPagePath}
+          onClose={onClose}
+        />)
+      }
+    </>
   );
   );
 
 
 };
 };

+ 3 - 3
apps/app/src/components/PageHistory/Revision.tsx

@@ -13,10 +13,10 @@ import styles from './Revision.module.scss';
 
 
 type RevisionProps = {
 type RevisionProps = {
   revision: IRevisionHasId,
   revision: IRevisionHasId,
-  currentPageId: string,
-  currentPagePath: string,
   isLatestRevision: boolean,
   isLatestRevision: boolean,
   hasDiff: boolean,
   hasDiff: boolean,
+  currentPageId: string
+  currentPagePath: string
   onClose: () => void,
   onClose: () => void,
 }
 }
 
 
@@ -24,7 +24,7 @@ export const Revision = (props: RevisionProps): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
 
 
   const {
   const {
-    revision, currentPageId, currentPagePath, isLatestRevision, hasDiff, onClose,
+    revision, isLatestRevision, hasDiff, onClose, currentPageId, currentPagePath,
   } = props;
   } = props;
 
 
   const { returnPathForURL } = pathUtils;
   const { returnPathForURL } = pathUtils;

+ 2 - 1
apps/app/src/components/RevisionComparer/RevisionComparer.tsx

@@ -32,11 +32,12 @@ export const RevisionComparer = (props: RevisionComparerProps): JSX.Element => {
   const { t } = useTranslation(['translation', 'commons']);
   const { t } = useTranslation(['translation', 'commons']);
 
 
   const {
   const {
-    sourceRevision, targetRevision, currentPageId, currentPagePath, onClose,
+    sourceRevision, targetRevision, onClose, currentPageId, currentPagePath,
   } = props;
   } = props;
 
 
   const [dropdownOpen, setDropdownOpen] = useState(false);
   const [dropdownOpen, setDropdownOpen] = useState(false);
 
 
+
   const toggleDropdown = () => {
   const toggleDropdown = () => {
     setDropdownOpen(!dropdownOpen);
     setDropdownOpen(!dropdownOpen);
   };
   };

+ 1 - 1
apps/app/src/components/Sidebar/RecentChanges.tsx

@@ -14,8 +14,8 @@ import { useSWRINFxRecentlyUpdated } from '~/stores/page-listing';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import FormattedDistanceDate from '../FormattedDistanceDate';
 import FormattedDistanceDate from '../FormattedDistanceDate';
+import InfiniteScroll from '../InfiniteScroll';
 
 
-import InfiniteScroll from './InfiniteScroll';
 import { SidebarHeaderReloadButton } from './SidebarHeaderReloadButton';
 import { SidebarHeaderReloadButton } from './SidebarHeaderReloadButton';
 import RecentChangesContentSkeleton from './Skeleton/RecentChangesContentSkeleton';
 import RecentChangesContentSkeleton from './Skeleton/RecentChangesContentSkeleton';
 
 

+ 5 - 1
apps/app/src/server/routes/apiv3/pages.js

@@ -871,8 +871,12 @@ module.exports = (crowi) => {
     }
     }
 
 
     // run delete
     // run delete
+    const activityParameters = {
+      ip: req.ip,
+      endpoint: req.originalUrl,
+    };
     const options = { isCompletely, isRecursively };
     const options = { isCompletely, isRecursively };
-    crowi.pageService.deleteMultiplePages(pagesCanBeDeleted, req.user, options);
+    crowi.pageService.deleteMultiplePages(pagesCanBeDeleted, req.user, options, activityParameters);
 
 
     return res.apiv3({ paths: pagesCanBeDeleted.map(p => p.path), isRecursively, isCompletely });
     return res.apiv3({ paths: pagesCanBeDeleted.map(p => p.path), isRecursively, isCompletely });
   });
   });

+ 21 - 10
apps/app/src/server/routes/apiv3/revisions.js

@@ -72,7 +72,7 @@ module.exports = (crowi) => {
   const validator = {
   const validator = {
     retrieveRevisions: [
     retrieveRevisions: [
       query('pageId').isMongoId().withMessage('pageId is required'),
       query('pageId').isMongoId().withMessage('pageId is required'),
-      query('page').isInt({ min: 0 }).withMessage('page must be int'),
+      query('offset').if(value => value != null).isInt({ min: 0 }).withMessage('offset must be int'),
       query('limit').if(value => value != null).isInt({ max: 100 }).withMessage('You should set less than 100 or not to set limit.'),
       query('limit').if(value => value != null).isInt({ max: 100 }).withMessage('You should set less than 100 or not to set limit.'),
 
 
     ],
     ],
@@ -114,8 +114,7 @@ module.exports = (crowi) => {
     const pageId = req.query.pageId;
     const pageId = req.query.pageId;
     const limit = req.query.limit || await crowi.configManager.getConfig('crowi', 'customize:showPageLimitationS') || 10;
     const limit = req.query.limit || await crowi.configManager.getConfig('crowi', 'customize:showPageLimitationS') || 10;
     const { isSharedPage } = req;
     const { isSharedPage } = req;
-
-    const selectedPage = parseInt(req.query.page) || 1;
+    const offset = req.query.offset || 0;
 
 
     // check whether accessible
     // check whether accessible
     if (!isSharedPage && !(await Page.isAccessiblePageByViewer(pageId, req.user))) {
     if (!isSharedPage && !(await Page.isAccessiblePageByViewer(pageId, req.user))) {
@@ -124,15 +123,21 @@ module.exports = (crowi) => {
 
 
     try {
     try {
       const page = await Page.findOne({ _id: pageId });
       const page = await Page.findOne({ _id: pageId });
+      const queryOpts = {
+        offset,
+        sort: { createdAt: -1 },
+        populate: 'author',
+        pagination: false,
+      };
+
+      if (limit > 0) {
+        queryOpts.limit = limit;
+        queryOpts.pagination = true;
+      }
 
 
       const paginateResult = await Revision.paginate(
       const paginateResult = await Revision.paginate(
         { pageId: page._id },
         { pageId: page._id },
-        {
-          page: selectedPage,
-          limit,
-          sort: { createdAt: -1 },
-          populate: 'author',
-        },
+        queryOpts,
       );
       );
 
 
       paginateResult.docs.forEach((doc) => {
       paginateResult.docs.forEach((doc) => {
@@ -141,7 +146,13 @@ module.exports = (crowi) => {
         }
         }
       });
       });
 
 
-      return res.apiv3(paginateResult);
+      const result = {
+        revisions: paginateResult.docs,
+        totalCount: paginateResult.totalDocs,
+        offset: paginateResult.offset,
+      };
+
+      return res.apiv3(result);
     }
     }
     catch (err) {
     catch (err) {
       const msg = 'Error occurred in getting revisions by poge id';
       const msg = 'Error occurred in getting revisions by poge id';

+ 2 - 2
apps/app/src/server/routes/attachment.js

@@ -1,4 +1,3 @@
-
 import { SupportedAction } from '~/interfaces/activity';
 import { SupportedAction } from '~/interfaces/activity';
 import { AttachmentType } from '~/server/interfaces/attachment';
 import { AttachmentType } from '~/server/interfaces/attachment';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
@@ -452,6 +451,7 @@ module.exports = function(crowi, app) {
   api.add = async function(req, res) {
   api.add = async function(req, res) {
     let pageId = req.body.page_id || null;
     let pageId = req.body.page_id || null;
     const pagePath = req.body.path || null;
     const pagePath = req.body.path || null;
+    const pageBody = req.body.page_body || null;
     let pageCreated = false;
     let pageCreated = false;
 
 
     // check params
     // check params
@@ -471,7 +471,7 @@ module.exports = function(crowi, app) {
       const isAclEnabled = crowi.aclService.isAclEnabled();
       const isAclEnabled = crowi.aclService.isAclEnabled();
       const grant = isAclEnabled ? Page.GRANT_OWNER : Page.GRANT_PUBLIC;
       const grant = isAclEnabled ? Page.GRANT_OWNER : Page.GRANT_PUBLIC;
 
 
-      page = await crowi.pageService.create(pagePath, `# ${pagePath}`, req.user, { grant });
+      page = await crowi.pageService.create(pagePath, pageBody ?? '', req.user, { grant });
       pageCreated = true;
       pageCreated = true;
       pageId = page._id;
       pageId = page._id;
     }
     }

+ 5 - 5
apps/app/src/server/service/page.ts

@@ -1363,7 +1363,7 @@ class PageService {
   /*
   /*
    * Delete
    * Delete
    */
    */
-  async deletePage(page, user, options = {}, isRecursively = false, activityParameters?) {
+  async deletePage(page, user, options = {}, isRecursively = false, activityParameters) {
     /*
     /*
      * Common Operation
      * Common Operation
      */
      */
@@ -1722,7 +1722,7 @@ class PageService {
     return;
     return;
   }
   }
 
 
-  async deleteCompletely(page, user, options = {}, isRecursively = false, preventEmitting = false, activityParameters?) {
+  async deleteCompletely(page, user, options = {}, isRecursively = false, preventEmitting = false, activityParameters) {
     /*
     /*
      * Common Operation
      * Common Operation
      */
      */
@@ -1939,7 +1939,7 @@ class PageService {
   }
   }
 
 
   // no need to separate Main Sub since it is devided into single page operations
   // no need to separate Main Sub since it is devided into single page operations
-  async deleteMultiplePages(pagesToDelete, user, options): Promise<void> {
+  async deleteMultiplePages(pagesToDelete, user, options, activityParameters): Promise<void> {
     const { isRecursively, isCompletely } = options;
     const { isRecursively, isCompletely } = options;
 
 
     if (pagesToDelete.length > LIMIT_FOR_MULTIPLE_PAGE_OP) {
     if (pagesToDelete.length > LIMIT_FOR_MULTIPLE_PAGE_OP) {
@@ -1951,12 +1951,12 @@ class PageService {
 
 
     if (isCompletely) {
     if (isCompletely) {
       for await (const page of pages) {
       for await (const page of pages) {
-        await this.deleteCompletely(page, user, {}, isRecursively);
+        await this.deleteCompletely(page, user, {}, isRecursively, false, activityParameters);
       }
       }
     }
     }
     else {
     else {
       for await (const page of pages) {
       for await (const page of pages) {
-        await this.deletePage(page, user, {}, isRecursively);
+        await this.deletePage(page, user, {}, isRecursively, activityParameters);
       }
       }
     }
     }
   }
   }

+ 25 - 18
apps/app/src/stores/page.tsx

@@ -1,11 +1,12 @@
-import { useEffect, useMemo, useCallback } from 'react';
+import { useEffect, useMemo } from 'react';
 
 
 import type {
 import type {
-  IPageInfoForEntity, IPagePopulatedToShowRevision, Nullable,
+  IPageInfoForEntity, IPagePopulatedToShowRevision, Nullable, SWRInfinitePageRevisionsResponse,
 } from '@growi/core';
 } from '@growi/core';
 import { Ref, isClient, pagePathUtils } from '@growi/core';
 import { Ref, isClient, pagePathUtils } from '@growi/core';
 import useSWR, { mutate, SWRResponse } from 'swr';
 import useSWR, { mutate, SWRResponse } from 'swr';
 import useSWRImmutable from 'swr/immutable';
 import useSWRImmutable from 'swr/immutable';
+import useSWRInfinite, { SWRInfiniteResponse } from 'swr/infinite';
 import useSWRMutation, { SWRMutationResponse } from 'swr/mutation';
 import useSWRMutation, { SWRMutationResponse } from 'swr/mutation';
 
 
 import { apiGet } from '~/client/util/apiv1-client';
 import { apiGet } from '~/client/util/apiv1-client';
@@ -14,7 +15,7 @@ import {
   IPageInfo, IPageInfoForOperation,
   IPageInfo, IPageInfoForOperation,
 } from '~/interfaces/page';
 } from '~/interfaces/page';
 import { IRecordApplicableGrant, IResIsGrantNormalized } from '~/interfaces/page-grant';
 import { IRecordApplicableGrant, IResIsGrantNormalized } from '~/interfaces/page-grant';
-import { IRevision, IRevisionHasId, IRevisionsForPagination } from '~/interfaces/revision';
+import { IRevision, IRevisionHasId } from '~/interfaces/revision';
 
 
 import { IPageTagsInfo } from '../interfaces/tag';
 import { IPageTagsInfo } from '../interfaces/tag';
 
 
@@ -143,22 +144,28 @@ export const useSWRxPageRevision = (pageId: string, revisionId: Ref<IRevision>):
   );
   );
 };
 };
 
 
-export const useSWRxPageRevisions = (
-    page: number, // page number of pagination
-    limit: number, // max number of pages in one paginate
-    pageId: string | null | undefined,
-): SWRResponse<IRevisionsForPagination, Error> => {
+/*
+ * SWR Infinite for page revision list
+ */
 
 
-  return useSWRImmutable(
-    ['/revisions/list', pageId, page, limit],
-    ([endpoint, pageId, page, limit]) => {
-      return apiv3Get(endpoint, { pageId, page, limit }).then((response) => {
-        const revisions = {
-          revisions: response.data.docs,
-          totalCounts: response.data.totalDocs,
-        };
-        return revisions;
-      });
+export const useSWRxInfinitePageRevisions = (
+    pageId: string,
+    limit: number,
+): SWRInfiniteResponse<SWRInfinitePageRevisionsResponse, Error> => {
+  return useSWRInfinite(
+    (pageIndex, previousRevisionData) => {
+      if (previousRevisionData != null && previousRevisionData.revisions.length === 0) return null;
+
+      if (pageIndex === 0 || previousRevisionData == null) {
+        return ['/revisions/list', pageId, undefined, limit];
+      }
+      const offset = previousRevisionData.offset + limit;
+      return ['/revisions/list', pageId, offset, limit];
+    },
+    ([endpoint, pageId, offset, limit]) => apiv3Get<SWRInfinitePageRevisionsResponse>(endpoint, { pageId, offset, limit }).then(response => response.data),
+    {
+      revalidateFirstPage: true,
+      revalidateAll: false,
     },
     },
   );
   );
 };
 };

+ 6 - 0
packages/core/src/interfaces/revision.ts

@@ -32,3 +32,9 @@ export type IRevisionOnConflict = {
 export type HasRevisionShortbody = {
 export type HasRevisionShortbody = {
   revisionShortBody?: string,
   revisionShortBody?: string,
 }
 }
+
+export type SWRInfinitePageRevisionsResponse = {
+  revisions: IRevisionHasPageId[],
+  totalCount: number,
+  offset: number,
+}