Sfoglia il codice sorgente

Merge remote-tracking branch 'origin/master' into
imprv/88633-do-not-move-under-user-page

kaori 4 anni fa
parent
commit
4960e68784

+ 2 - 0
packages/app/docker/README.md

@@ -10,6 +10,8 @@ GROWI Official docker image
 Supported tags and respective Dockerfile links
 ------------------------------------------------
 
+* [`5.0.0`, `5.0`, `5`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v5.0.0/docker/Dockerfile)
+* [`5.0.0-nocdn`, `5.0-nocdn`, `5-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v5.0.0/docker/Dockerfile)
 * [`4.5.14`, `4.5`, `4`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.14/docker/Dockerfile)
 * [`4.5.14-nocdn`, `4.5-nocdn`, `4-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.14/docker/Dockerfile)
 * [`4.4.13`, `4.4` (Dockerfile)](https://github.com/weseek/growi/blob/v4.4.13/docker/Dockerfile)

+ 4 - 0
packages/app/src/client/services/ContextExtractor.tsx

@@ -14,6 +14,7 @@ import {
   usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser, useSidebarCollapsed, useCurrentSidebarContents, useCurrentProductNavWidth,
   useSelectedGrant, useSelectedGrantGroupId, useSelectedGrantGroupName,
 } from '~/stores/ui';
+import { useSetupGlobalSocket } from '~/stores/websocket';
 import { IUserUISettings } from '~/interfaces/user-ui-settings';
 
 const { isTrashPage: _isTrashPage } = pagePathUtils;
@@ -156,6 +157,9 @@ const ContextExtractorOnce: FC = () => {
   // SearchResult
   useIsDeviceSmallerThanLg();
 
+  // Global Socket
+  useSetupGlobalSocket();
+
   return null;
 };
 

+ 9 - 3
packages/app/src/components/Sidebar/PageTree/Item.tsx

@@ -23,6 +23,7 @@ import { bookmark, unbookmark } from '~/client/services/page-operation';
 import ClosableTextInput, { AlertInfo, AlertType } from '../../Common/ClosableTextInput';
 import { PageItemControl } from '../../Common/Dropdown/PageItemControl';
 import { ItemNode } from './ItemNode';
+import { usePageTreeDescCountMap } from '~/stores/ui';
 import { IPageHasId, IPageInfoAll, IPageToDeleteWithMeta } from '~/interfaces/page';
 
 
@@ -126,9 +127,14 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
 
   const { data, mutate: mutateChildren } = useSWRxPageChildren(isOpen ? page._id : null);
 
+  // descendantCount
+  const { getDescCount } = usePageTreeDescCountMap();
+  const descendantCount = getDescCount(page._id) || page.descendantCount || 0;
+
+
   // hasDescendants flag
   const isChildrenLoaded = currentChildren?.length > 0;
-  const hasDescendants = (page.descendantCount != null && page?.descendantCount > 0) || isChildrenLoaded;
+  const hasDescendants = descendantCount > 0 || isChildrenLoaded;
 
   // to re-show hidden item when useDrag end() callback
   const displayDroppedItemByPageId = useCallback((pageId) => {
@@ -446,9 +452,9 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
           <p className={`text-truncate m-auto ${page.isEmpty && 'text-muted'}`}>{nodePath.basename(pageTitle as string) || '/'}</p>
         </a>
         {/* )} */}
-        {(page.descendantCount != null && page.descendantCount > 0) && (
+        {(descendantCount > 0) && (
           <div className="grw-pagetree-count-wrapper">
-            <ItemCount descendantCount={page.descendantCount} />
+            <ItemCount descendantCount={descendantCount} />
           </div>
         )}
         <div className="grw-pagetree-control d-flex">

+ 20 - 0
packages/app/src/components/Sidebar/PageTree/ItemsTree.tsx

@@ -5,6 +5,7 @@ import { usePageTreeTermManager, useSWRxPageAncestorsChildren, useSWRxRootPage }
 import { TargetAndAncestors } from '~/interfaces/page-listing-results';
 import { IPageHasId, IPageToDeleteWithMeta } from '~/interfaces/page';
 import { OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction } from '~/interfaces/ui';
+import { SocketEventName, UpdateDescCountData, UpdateDescCountRawData } from '~/interfaces/websocket';
 import { toastError, toastSuccess } from '~/client/util/apiNotification';
 import {
   IPageForPageDuplicateModal, usePageDuplicateModal, IPageForPageRenameModal, usePageRenameModal, usePageDeleteModal,
@@ -14,6 +15,8 @@ import { smoothScrollIntoView } from '~/client/util/smooth-scroll';
 import { useIsEnabledAttachTitleHeader } from '~/stores/context';
 import { useFullTextSearchTermManager } from '~/stores/search';
 import { useDescendantsPageListForCurrentPathTermManager } from '~/stores/page';
+import { useGlobalSocket } from '~/stores/websocket';
+import { usePageTreeDescCountMap } from '~/stores/ui';
 
 import { ItemNode } from './ItemNode';
 import Item from './Item';
@@ -126,6 +129,9 @@ const ItemsTree: FC<ItemsTreeProps> = (props: ItemsTreeProps) => {
   const { open: openDeleteModal } = usePageDeleteModal();
   const [isScrolled, setIsScrolled] = useState(false);
 
+  const { data: socket } = useGlobalSocket();
+  const { data: ptDescCountMap, update: updatePtDescCountMap } = usePageTreeDescCountMap();
+
 
   // for mutation
   const { advance: advancePt } = usePageTreeTermManager();
@@ -144,6 +150,20 @@ const ItemsTree: FC<ItemsTreeProps> = (props: ItemsTreeProps) => {
     };
   }, []);
 
+  useEffect(() => {
+    if (socket == null) {
+      return;
+    }
+
+    // socket
+    socket.on(SocketEventName.UpdateDescCount, (data: UpdateDescCountRawData) => {
+      // save to global state
+      const newData: UpdateDescCountData = new Map(Object.entries(data));
+
+      updatePtDescCountMap(newData);
+    });
+  }, [socket, ptDescCountMap, updatePtDescCountMap]);
+
   const onClickDuplicateMenuItem = (pageToDuplicate: IPageForPageDuplicateModal) => {
     // eslint-disable-next-line @typescript-eslint/no-unused-vars
     const duplicatedHandler: OnDuplicatedFunction = (fromPath, toPath) => {

+ 12 - 0
packages/app/src/interfaces/websocket.ts

@@ -0,0 +1,12 @@
+export const SocketEventName = {
+  UpdateDescCount: 'UpdateDsecCount',
+} as const;
+export type SocketEventName = typeof SocketEventName[keyof typeof SocketEventName];
+
+type PageId = string;
+type DescendantCount = number;
+/**
+ * Data of updateDescCount when used through socket.io. Convert to UpdateDescCountData type when use with swr cache.
+ */
+export type UpdateDescCountRawData = Record<PageId, DescendantCount>;
+export type UpdateDescCountData = Map<PageId, DescendantCount>;

+ 1 - 1
packages/app/src/server/models/obsolete-page.js

@@ -330,7 +330,7 @@ export class PageQueryBuilder {
   }
 
   addConditionToMinimizeDataForRendering() {
-    this.query = this.query.select('_id path isEmpty grant revision');
+    this.query = this.query.select('_id path isEmpty grant revision descendantCount');
 
     return this;
   }

+ 11 - 0
packages/app/src/server/service/page.ts

@@ -21,6 +21,7 @@ import { ObjectIdLike } from '../interfaces/mongoose-utils';
 import { IUserHasId } from '~/interfaces/user';
 import { Ref } from '~/interfaces/common';
 import { HasObjectId } from '~/interfaces/has-object-id';
+import { SocketEventName, UpdateDescCountRawData } from '~/interfaces/websocket';
 import PageOperation, { PageActionStage, PageActionType } from '../models/page-operation';
 import ActivityDefine from '../util/activityDefine';
 
@@ -2635,7 +2636,17 @@ class PageService {
     const Page = this.crowi.model('Page');
     const ancestors = await Page.findAncestorsUsingParentRecursively(pageId, shouldIncludeTarget);
     const ancestorPageIds = ancestors.map(p => p._id);
+
     await Page.incrementDescendantCountOfPageIds(ancestorPageIds, inc);
+
+    const updateDescCountData: UpdateDescCountRawData = Object.fromEntries(ancestors.map(p => [p._id.toString(), p.descendantCount + inc]));
+    this.emitUpdateDescCount(updateDescCountData);
+  }
+
+  private emitUpdateDescCount(data: UpdateDescCountRawData): void {
+    const socket = this.crowi.socketIoService.getDefaultSocket();
+
+    socket.emit(SocketEventName.UpdateDescCount, data);
   }
 
 }

+ 18 - 0
packages/app/src/stores/ui.tsx

@@ -17,6 +17,7 @@ import {
 } from './context';
 import { IFocusable } from '~/client/interfaces/focusable';
 import { Nullable } from '~/interfaces/common';
+import { UpdateDescCountData } from '~/interfaces/websocket';
 
 const { isSharedPage } = pagePathUtils;
 
@@ -334,3 +335,20 @@ export const useIsAbleToShowPageAuthors = (): SWRResponse<boolean, Error> => {
     () => isPageExist && !isUserPage,
   );
 };
+
+type PageTreeDescCountMapUtils = {
+  update(newData?: UpdateDescCountData): Promise<UpdateDescCountData | undefined>
+  getDescCount(pageId?: string): number | null | undefined
+}
+
+export const usePageTreeDescCountMap = (initialData?: UpdateDescCountData): SWRResponse<UpdateDescCountData, Error> & PageTreeDescCountMapUtils => {
+  const key = 'pageTreeDescCountMap';
+
+  const swrResponse = useStaticSWR<UpdateDescCountData, Error>(key, initialData, { fallbackData: new Map() });
+
+  return {
+    ...swrResponse,
+    getDescCount: (pageId?: string) => (pageId != null ? swrResponse.data?.get(pageId) : null),
+    update: (newData: UpdateDescCountData) => swrResponse.mutate(new Map([...(swrResponse.data || new Map()), ...newData])),
+  };
+};

+ 25 - 0
packages/app/src/stores/websocket.tsx

@@ -0,0 +1,25 @@
+import { SWRResponse } from 'swr';
+import io, { Socket } from 'socket.io-client';
+
+import { useStaticSWR } from './use-static-swr';
+import loggerFactory from '~/utils/logger';
+
+const logger = loggerFactory('growi:stores:ui');
+
+export const GLOBAL_SOCKET_NS = '/';
+export const GLOBAL_SOCKET_KEY = 'globalSocket';
+
+export const useSetupGlobalSocket = (): SWRResponse<Socket, Error> => {
+  const socket = io(GLOBAL_SOCKET_NS, {
+    transports: ['websocket'],
+  });
+
+  socket.on('error', (err) => { logger.error(err) });
+  socket.on('connect_error', (err) => { logger.error('Failed to connect with websocket.', err) });
+
+  return useStaticSWR(GLOBAL_SOCKET_KEY, socket);
+};
+
+export const useGlobalSocket = (): SWRResponse<Socket, Error> => {
+  return useStaticSWR(GLOBAL_SOCKET_KEY);
+};

+ 3 - 0
packages/app/test/integration/setup-crowi.js

@@ -1,3 +1,5 @@
+import { Server } from 'http';
+
 import Crowi from '~/server/crowi';
 
 let _instance = null;
@@ -7,6 +9,7 @@ const initCrowi = async(crowi) => {
   await crowi.setupConfigManager();
 
   await crowi.setupSocketIoService();
+  await crowi.socketIoService.attachServer(new Server()); // attach dummy server
 
   await Promise.all([
     crowi.setUpApp(),