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

Merge remote-tracking branch 'origin/master' into imprv/closable-text-input-autosizing

Yuki Takei 1 год назад
Родитель
Сommit
a48bca5acc

+ 3 - 57
apps/app/src/components/ItemsTree/ItemsTree.tsx

@@ -1,5 +1,5 @@
 import React, {
 import React, {
-  useEffect, useRef, useState, useMemo, useCallback,
+  useEffect, useMemo, useCallback,
 } from 'react';
 } from 'react';
 
 
 import path from 'path';
 import path from 'path';
@@ -8,7 +8,6 @@ import type { Nullable, IPageHasId, IPageToDeleteWithMeta } from '@growi/core';
 import { useGlobalSocket } from '@growi/core/dist/swr';
 import { useGlobalSocket } from '@growi/core/dist/swr';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import { useRouter } from 'next/router';
 import { useRouter } from 'next/router';
-import { debounce } from 'throttle-debounce';
 
 
 import { toastError, toastSuccess } from '~/client/util/toastr';
 import { toastError, toastSuccess } from '~/client/util/toastr';
 import type { IPageForItem } from '~/interfaces/page';
 import type { IPageForItem } from '~/interfaces/page';
@@ -23,7 +22,7 @@ import {
   useSWRxPageAncestorsChildren, useSWRxRootPage, mutatePageTree, mutatePageList,
   useSWRxPageAncestorsChildren, useSWRxRootPage, mutatePageTree, mutatePageList,
 } from '~/stores/page-listing';
 } from '~/stores/page-listing';
 import { mutateSearching } from '~/stores/search';
 import { mutateSearching } from '~/stores/search';
-import { usePageTreeDescCountMap, useSidebarScrollerRef } from '~/stores/ui';
+import { usePageTreeDescCountMap } from '~/stores/ui';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import { ItemNode, type TreeItemProps } from '../TreeItem';
 import { ItemNode, type TreeItemProps } from '../TreeItem';
@@ -116,7 +115,6 @@ export const ItemsTree = (props: ItemsTreeProps): JSX.Element => {
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: currentPagePath } = useCurrentPagePath();
   const { open: openDuplicateModal } = usePageDuplicateModal();
   const { open: openDuplicateModal } = usePageDuplicateModal();
   const { open: openDeleteModal } = usePageDeleteModal();
   const { open: openDeleteModal } = usePageDeleteModal();
-  const { data: sidebarScrollerRef } = useSidebarScrollerRef();
 
 
   const { data: socket } = useGlobalSocket();
   const { data: socket } = useGlobalSocket();
   const { data: ptDescCountMap, update: updatePtDescCountMap } = usePageTreeDescCountMap();
   const { data: ptDescCountMap, update: updatePtDescCountMap } = usePageTreeDescCountMap();
@@ -124,9 +122,6 @@ export const ItemsTree = (props: ItemsTreeProps): JSX.Element => {
   // for mutation
   // for mutation
   const { trigger: mutateCurrentPage } = useSWRMUTxCurrentPage();
   const { trigger: mutateCurrentPage } = useSWRMUTxCurrentPage();
 
 
-  const [isInitialScrollCompleted, setIsInitialScrollCompleted] = useState(false);
-
-  const rootElemRef = useRef(null);
 
 
   const renderingCondition = useMemo(() => {
   const renderingCondition = useMemo(() => {
     return {
     return {
@@ -201,55 +196,6 @@ export const ItemsTree = (props: ItemsTreeProps): JSX.Element => {
     openDeleteModal([pageToDelete], { onDeleted: onDeletedHandler });
     openDeleteModal([pageToDelete], { onDeleted: onDeletedHandler });
   }, [currentPagePath, mutateCurrentPage, openDeleteModal, router, t]);
   }, [currentPagePath, mutateCurrentPage, openDeleteModal, router, t]);
 
 
-  // ***************************  Scroll on init ***************************
-  const scrollOnInit = useCallback(() => {
-    const scrollTargetElement = document.getElementById('grw-pagetree-current-page-item');
-
-    if (sidebarScrollerRef?.current == null || scrollTargetElement == null) {
-      return;
-    }
-
-    logger.debug('scrollOnInit has invoked');
-
-    const scrollElement = sidebarScrollerRef.current.getScrollElement();
-
-    // NOTE: could not use scrollIntoView
-    //  https://stackoverflow.com/questions/11039885/scrollintoview-causing-the-whole-page-to-move
-
-    // calculate the center point
-    const scrollTop = scrollTargetElement.offsetTop - scrollElement.getBoundingClientRect().height / 2;
-    scrollElement.scrollTo({ top: scrollTop });
-
-    setIsInitialScrollCompleted(true);
-  }, [sidebarScrollerRef]);
-
-  const scrollOnInitDebounced = useMemo(() => debounce(500, scrollOnInit), [scrollOnInit]);
-
-  useEffect(() => {
-    if (!isSecondStageRenderingCondition(renderingCondition) || isInitialScrollCompleted) {
-      return;
-    }
-
-    const rootElement = rootElemRef.current as HTMLElement | null;
-    if (rootElement == null) {
-      return;
-    }
-
-    const observerCallback = (mutationRecords: MutationRecord[]) => {
-      mutationRecords.forEach(() => scrollOnInitDebounced());
-    };
-
-    const observer = new MutationObserver(observerCallback);
-    observer.observe(rootElement, { childList: true, subtree: true });
-
-    // first call for the situation that all rendering is complete at this point
-    scrollOnInitDebounced();
-
-    return () => {
-      observer.disconnect();
-    };
-  }, [isInitialScrollCompleted, renderingCondition, scrollOnInitDebounced]);
-  // *******************************  end  *******************************
 
 
   if (error1 != null || error2 != null) {
   if (error1 != null || error2 != null) {
     // TODO: improve message
     // TODO: improve message
@@ -276,7 +222,7 @@ export const ItemsTree = (props: ItemsTreeProps): JSX.Element => {
 
 
   if (initialItemNode != null) {
   if (initialItemNode != null) {
     return (
     return (
-      <ul className={`${moduleClass} list-group py-4`} ref={rootElemRef}>
+      <ul className={`${moduleClass} list-group py-4`}>
         <CustomTreeItem
         <CustomTreeItem
           key={initialItemNode.page.path}
           key={initialItemNode.page.path}
           targetPathOrId={targetPathOrId}
           targetPathOrId={targetPathOrId}

+ 72 - 5
apps/app/src/components/Sidebar/PageTree/PageTreeSubstance.tsx

@@ -1,13 +1,20 @@
-import React, { memo, useCallback } from 'react';
+import React, {
+  memo, useCallback, useEffect, useMemo, useRef, useState,
+} from 'react';
 
 
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import {
 import {
   UncontrolledButtonDropdown, DropdownMenu, DropdownToggle, DropdownItem,
   UncontrolledButtonDropdown, DropdownMenu, DropdownToggle, DropdownItem,
 } from 'reactstrap';
 } from 'reactstrap';
+import { debounce } from 'throttle-debounce';
 
 
 import { useTargetAndAncestors, useIsGuestUser, useIsReadOnlyUser } from '~/stores/context';
 import { useTargetAndAncestors, useIsGuestUser, useIsReadOnlyUser } from '~/stores/context';
 import { useCurrentPagePath, useCurrentPageId } from '~/stores/page';
 import { useCurrentPagePath, useCurrentPageId } from '~/stores/page';
-import { mutatePageTree, useSWRxRootPage, useSWRxV5MigrationStatus } from '~/stores/page-listing';
+import {
+  mutatePageTree, useSWRxPageAncestorsChildren, useSWRxRootPage, useSWRxV5MigrationStatus,
+} from '~/stores/page-listing';
+import { useSidebarScrollerRef } from '~/stores/ui';
+import loggerFactory from '~/utils/logger';
 
 
 import { ItemsTree } from '../../ItemsTree/ItemsTree';
 import { ItemsTree } from '../../ItemsTree/ItemsTree';
 import { PageTreeItem } from '../PageTreeItem';
 import { PageTreeItem } from '../PageTreeItem';
@@ -15,6 +22,8 @@ import { SidebarHeaderReloadButton } from '../SidebarHeaderReloadButton';
 
 
 import { PrivateLegacyPagesLink } from './PrivateLegacyPagesLink';
 import { PrivateLegacyPagesLink } from './PrivateLegacyPagesLink';
 
 
+const logger = loggerFactory('growi:cli:PageTreeSubstance');
+
 type HeaderProps = {
 type HeaderProps = {
   isWipPageShown: boolean,
   isWipPageShown: boolean,
   onWipPageShownChange?: () => void
   onWipPageShownChange?: () => void
@@ -91,6 +100,65 @@ export const PageTreeContent = memo(({ isWipPageShown }: PageTreeContentProps) =
   const { data: migrationStatus } = useSWRxV5MigrationStatus({ suspense: true });
   const { data: migrationStatus } = useSWRxV5MigrationStatus({ suspense: true });
 
 
   const targetPathOrId = targetId || currentPath;
   const targetPathOrId = targetId || currentPath;
+  const path = currentPath || '/';
+
+  const { data: ancestorsChildrenResult } = useSWRxPageAncestorsChildren(path, { suspense: true });
+  const { data: rootPageResult } = useSWRxRootPage({ suspense: true });
+  const { data: sidebarScrollerRef } = useSidebarScrollerRef();
+  const [isInitialScrollCompleted, setIsInitialScrollCompleted] = useState(false);
+
+  const rootElemRef = useRef(null);
+
+  // ***************************  Scroll on init ***************************
+  const scrollOnInit = useCallback(() => {
+    const scrollTargetElement = document.getElementById('grw-pagetree-current-page-item');
+
+    if (sidebarScrollerRef?.current == null || scrollTargetElement == null) {
+      return;
+    }
+
+    logger.debug('scrollOnInit has invoked');
+
+    const scrollElement = sidebarScrollerRef.current;
+
+    // NOTE: could not use scrollIntoView
+    //  https://stackoverflow.com/questions/11039885/scrollintoview-causing-the-whole-page-to-move
+
+    // calculate the center point
+    const scrollTop = scrollTargetElement.offsetTop - scrollElement.getBoundingClientRect().height / 2;
+    scrollElement.scrollTo({ top: scrollTop });
+
+    setIsInitialScrollCompleted(true);
+  }, [sidebarScrollerRef]);
+
+  const scrollOnInitDebounced = useMemo(() => debounce(500, scrollOnInit), [scrollOnInit]);
+
+  useEffect(() => {
+    if (isInitialScrollCompleted || ancestorsChildrenResult == null || rootPageResult == null) {
+      return;
+    }
+
+    const rootElement = rootElemRef.current as HTMLElement | null;
+    if (rootElement == null) {
+      return;
+    }
+
+    const observerCallback = (mutationRecords: MutationRecord[]) => {
+      mutationRecords.forEach(() => scrollOnInitDebounced());
+    };
+
+    const observer = new MutationObserver(observerCallback);
+    observer.observe(rootElement, { childList: true, subtree: true });
+
+    // first call for the situation that all rendering is complete at this point
+    scrollOnInitDebounced();
+
+    return () => {
+      observer.disconnect();
+    };
+  }, [isInitialScrollCompleted, scrollOnInitDebounced, ancestorsChildrenResult, rootPageResult]);
+  // *******************************  end  *******************************
+
 
 
   if (!migrationStatus?.isV5Compatible) {
   if (!migrationStatus?.isV5Compatible) {
     return <PageTreeUnavailable />;
     return <PageTreeUnavailable />;
@@ -103,10 +171,9 @@ export const PageTreeContent = memo(({ isWipPageShown }: PageTreeContentProps) =
     return null;
     return null;
   }
   }
 
 
-  const path = currentPath || '/';
 
 
   return (
   return (
-    <>
+    <div ref={rootElemRef}>
       <ItemsTree
       <ItemsTree
         isEnableActions={!isGuestUser}
         isEnableActions={!isGuestUser}
         isReadOnlyUser={!!isReadOnlyUser}
         isReadOnlyUser={!!isReadOnlyUser}
@@ -124,7 +191,7 @@ export const PageTreeContent = memo(({ isWipPageShown }: PageTreeContentProps) =
           </div>
           </div>
         </div>
         </div>
       )}
       )}
-    </>
+    </div>
   );
   );
 });
 });
 
 

+ 7 - 0
apps/app/src/components/Sidebar/Sidebar.tsx

@@ -1,6 +1,7 @@
 import React, {
 import React, {
   type FC,
   type FC,
   memo, useCallback, useEffect, useState,
   memo, useCallback, useEffect, useState,
+  useRef,
 } from 'react';
 } from 'react';
 
 
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
@@ -13,6 +14,7 @@ import {
   useCurrentProductNavWidth,
   useCurrentProductNavWidth,
   usePreferCollapsedMode,
   usePreferCollapsedMode,
   useSidebarMode,
   useSidebarMode,
+  useSidebarScrollerRef,
 } from '~/stores/ui';
 } from '~/stores/ui';
 
 
 import { DrawerToggler } from '../Common/DrawerToggler';
 import { DrawerToggler } from '../Common/DrawerToggler';
@@ -109,6 +111,10 @@ const CollapsibleContainer = memo((props: CollapsibleContainerProps): JSX.Elemen
   const { data: currentProductNavWidth } = useCurrentProductNavWidth();
   const { data: currentProductNavWidth } = useCurrentProductNavWidth();
   const { data: isCollapsedContentsOpened, mutate: mutateCollapsedContentsOpened } = useCollapsedContentsOpened();
   const { data: isCollapsedContentsOpened, mutate: mutateCollapsedContentsOpened } = useCollapsedContentsOpened();
 
 
+  const sidebarScrollerRef = useRef<HTMLDivElement>(null);
+  const { mutate: mutateSidebarScroller } = useSidebarScrollerRef();
+  mutateSidebarScroller(sidebarScrollerRef);
+
 
 
   // open menu when collapsed mode
   // open menu when collapsed mode
   const primaryItemHoverHandler = useCallback(() => {
   const primaryItemHoverHandler = useCallback(() => {
@@ -138,6 +144,7 @@ const CollapsibleContainer = memo((props: CollapsibleContainerProps): JSX.Elemen
     <div className={`flex-expand-horiz ${className}`} onMouseLeave={mouseLeaveHandler}>
     <div className={`flex-expand-horiz ${className}`} onMouseLeave={mouseLeaveHandler}>
       <Nav onPrimaryItemHover={primaryItemHoverHandler} />
       <Nav onPrimaryItemHover={primaryItemHoverHandler} />
       <div
       <div
+        ref={sidebarScrollerRef}
         className={`sidebar-contents-container flex-grow-1 overflow-y-auto overflow-x-hidden ${closedClass} ${openedClass}`}
         className={`sidebar-contents-container flex-grow-1 overflow-y-auto overflow-x-hidden ${closedClass} ${openedClass}`}
         style={{ width: collapsibleContentsWidth }}
         style={{ width: collapsibleContentsWidth }}
       >
       >

+ 1 - 1
apps/app/src/components/TreeItem/ItemNode.ts

@@ -1,4 +1,4 @@
-import { IPageForItem } from '../../interfaces/page';
+import type { IPageForItem } from '../../interfaces/page';
 
 
 export class ItemNode {
 export class ItemNode {
 
 

+ 4 - 4
apps/app/src/stores/ui.tsx

@@ -52,10 +52,6 @@ export type EditorMode = typeof EditorMode[keyof typeof EditorMode];
  *                     Storing objects to ref
  *                     Storing objects to ref
  *********************************************************** */
  *********************************************************** */
 
 
-export const useSidebarScrollerRef = (initialData?: RefObject<SimpleBar>): SWRResponse<RefObject<SimpleBar>, Error> => {
-  return useStaticSWR<RefObject<SimpleBar>, Error>('sidebarScrollerRef', initialData);
-};
-
 export const useCurrentPageTocNode = (): SWRResponse<HtmlElementNode, any> => {
 export const useCurrentPageTocNode = (): SWRResponse<HtmlElementNode, any> => {
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: currentPagePath } = useCurrentPagePath();
 
 
@@ -67,6 +63,10 @@ export const useCurrentPageTocNode = (): SWRResponse<HtmlElementNode, any> => {
  *                      for switching UI
  *                      for switching UI
  *********************************************************** */
  *********************************************************** */
 
 
+export const useSidebarScrollerRef = (initialData?: RefObject<HTMLDivElement>): SWRResponse<RefObject<HTMLDivElement>, Error> => {
+  return useSWRStatic<RefObject<HTMLDivElement>, Error>('sidebarScrollerRef', initialData);
+};
+
 export const useIsMobile = (): SWRResponse<boolean, Error> => {
 export const useIsMobile = (): SWRResponse<boolean, Error> => {
   const key = isClient() ? 'isMobile' : null;
   const key = isClient() ? 'isMobile' : null;