Przeglądaj źródła

Merge branch 'master' into suppoert/bump-swr-v2

Yuki Takei 3 lat temu
rodzic
commit
25b83f033b

+ 11 - 1
packages/app/src/components/DescendantsPageListModal.tsx

@@ -1,8 +1,9 @@
 
-import React, { useState, useMemo } from 'react';
+import React, { useState, useMemo, useEffect } from 'react';
 
 import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
+import { useRouter } from 'next/router';
 import {
   Modal, ModalHeader, ModalBody,
 } from 'reactstrap';
@@ -39,6 +40,15 @@ export const DescendantsPageListModal = (): JSX.Element => {
 
   const { data: status, close } = useDescendantsPageListModal();
 
+  const { events } = useRouter();
+
+  useEffect(() => {
+    events.on('routeChangeStart', close);
+    return () => {
+      events.off('routeChangeStart', close);
+    };
+  }, [close, events]);
+
   const navTabMapping = useMemo(() => {
     return {
       pagelist: {

+ 10 - 4
packages/app/src/components/Navbar/GrowiSubNavigationSwitcher.tsx

@@ -38,9 +38,11 @@ export const GrowiSubNavigationSwitcher = (props: GrowiSubNavigationSwitcherProp
   // use more specific type HTMLDivElement for avoid assertion error.
   // see: https://developer.mozilla.org/en-US/docs/Web/API/HTMLDivElement
   const fixedContainerRef = useRef<HTMLDivElement>(null);
+  const clientWidth = fixedContainerRef.current?.parentElement?.clientWidth;
 
+  // Do not use clientWidth as useCallback deps, resizing events will not work in production builds.
   const initWidth = useCallback(() => {
-    if (fixedContainerRef.current && fixedContainerRef.current.parentElement) {
+    if (fixedContainerRef.current != null && fixedContainerRef.current.parentElement != null) {
       // get parent elements width
       const { clientWidth } = fixedContainerRef.current.parentElement;
       setWidth(clientWidth);
@@ -84,10 +86,14 @@ export const GrowiSubNavigationSwitcher = (props: GrowiSubNavigationSwitcherProp
     }
   }, [isSidebarCollapsed, initWidth]);
 
-  // initialize width
+  /*
+   * initialize width.
+   * Since width is not recalculated at production build first rendering,
+   * make initWidth execution dependent on clientWidth.
+   */
   useEffect(() => {
-    initWidth();
-  }, [initWidth]);
+    if (clientWidth != null) initWidth();
+  }, [initWidth, clientWidth]);
 
   if (currentPage == null) {
     return <></>;

+ 11 - 8
packages/app/src/components/Page/PageView.tsx

@@ -17,6 +17,8 @@ import type { PageSideContentsProps } from '../PageSideContents';
 import { UserInfo } from '../User/UserInfo';
 import type { UsersHomePageFooterProps } from '../UsersHomePageFooter';
 
+import { PageContents } from './PageContents';
+
 import styles from './PageView.module.scss';
 
 
@@ -95,14 +97,15 @@ export const PageView = (props: Props): JSX.Element => {
 
   const contents = specialContents != null
     ? <></>
-    : (() => {
-      const PageContents = dynamic(() => import('./PageContents').then(mod => mod.PageContents), {
-        ssr: false,
-        // TODO: show SSR body
-        // loading: () => ssrBody ?? <></>,
-      });
-      return <PageContents />;
-    })();
+    // TODO: show SSR body
+    // : (() => {
+    //   const PageContents = dynamic(() => import('./PageContents').then(mod => mod.PageContents), {
+    //     ssr: false,
+    //     // loading: () => ssrBody ?? <></>,
+    //   });
+    //   return <PageContents />;
+    // })();
+    : <PageContents />;
 
   return (
     <MainPane

+ 15 - 2
packages/app/src/pages/utils/commons.ts

@@ -45,8 +45,21 @@ export const getServerSideCommonProps: GetServerSideProps<CommonProps> = async(c
     currentUser = user.toObject();
   }
 
-  // eslint-disable-next-line max-len, no-nested-ternary
-  const redirectDestination = !isMaintenanceMode && currentPathname === '/maintenance' ? '/' : isMaintenanceMode && !currentPathname.match('/admin/*') && !(currentPathname === '/maintenance') ? '/maintenance' : null;
+  // Redirect destination for page transition by next/link
+  let redirectDestination: string | null = null;
+  if (!crowi.aclService.isGuestAllowedToRead() && currentUser == null) {
+    redirectDestination = '/login';
+  }
+  else if (!isMaintenanceMode && currentPathname === '/maintenance') {
+    redirectDestination = '/';
+  }
+  else if (isMaintenanceMode && !currentPathname.match('/admin/*') && !(currentPathname === '/maintenance')) {
+    redirectDestination = '/maintenance';
+  }
+  else {
+    redirectDestination = null;
+  }
+
   const isCustomizedLogoUploaded = await attachmentService.isBrandLogoExist();
   const isDefaultLogo = crowi.configManager.getConfig('crowi', 'customize:isDefaultLogo') || !isCustomizedLogoUploaded;
   const forcedColorScheme = crowi.customizeService.forcedColorScheme;

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

@@ -599,7 +599,7 @@ module.exports = (crowi) => {
   router.delete('/empty-trash', accessTokenParser, loginRequired, addActivity, apiV3FormValidator, async(req, res) => {
     const options = {};
 
-    const pagesInTrash = await crowi.pageService.findChildrenByParentPathOrIdAndViewer('/trash', req.user);
+    const pagesInTrash = await crowi.pageService.findAllTrashPages(req.user);
 
     const deletablePages = crowi.pageService.filterPagesByCanDeleteCompletely(pagesInTrash, req.user, true);
 

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

@@ -4035,6 +4035,30 @@ class PageService {
     return pages;
   }
 
+  /**
+   * Find all pages in trash page
+   */
+  async findAllTrashPages(user: IUserHasId, userGroups = null): Promise<PageDocument[]> {
+    const Page = mongoose.model('Page') as unknown as PageModel;
+
+    // https://regex101.com/r/KYZWls/1
+    // ex. /trash/.*
+    const regexp = new RegExp('^/trash\\/.*$');
+    const queryBuilder = new PageQueryBuilder(Page.find({ path: { $regex: regexp } }), true);
+
+    await queryBuilder.addViewerCondition(user, userGroups);
+
+    const pages = await queryBuilder
+      .addConditionToSortPagesByAscPath()
+      .query
+      .lean()
+      .exec();
+
+    await this.injectProcessDataIntoPagesByActionTypes(pages, [PageActionType.Rename]);
+
+    return pages;
+  }
+
   async findAncestorsChildrenByPathAndViewer(path: string, user, userGroups = null): Promise<Record<string, PageDocument[]>> {
     const Page = mongoose.model('Page') as unknown as PageModel;
 

+ 7 - 2
packages/app/src/services/renderer/renderer.tsx

@@ -77,9 +77,14 @@ const commonSanitizeOption: SanitizeOption = deepmerge(
   },
 );
 
+let isInjectedCustomSanitaizeOption = false;
+
 const injectCustomSanitizeOption = (config: RendererConfig) => {
-  commonSanitizeOption.tagNames = config.tagWhiteList;
-  commonSanitizeOption.attributes = deepmerge(commonSanitizeAttributes, config.attrWhiteList ?? {});
+  if (!isInjectedCustomSanitaizeOption && config.isEnabledXssPrevention && config.xssOption === RehypeSanitizeOption.CUSTOM) {
+    commonSanitizeOption.tagNames = config.tagWhiteList;
+    commonSanitizeOption.attributes = deepmerge(commonSanitizeAttributes, config.attrWhiteList ?? {});
+    isInjectedCustomSanitaizeOption = true;
+  }
 };
 
 const isSanitizePlugin = (pluggable: Pluggable): pluggable is SanitizePlugin => {

+ 6 - 2
packages/app/src/stores/personal-settings.tsx

@@ -1,9 +1,9 @@
 import { useTranslation } from 'next-i18next';
 import useSWR, { SWRConfiguration, SWRResponse } from 'swr';
 
-
 import { IExternalAccount } from '~/interfaces/external-account';
 import { IUser } from '~/interfaces/user';
+import { useIsGuestUser } from '~/stores/context';
 import loggerFactory from '~/utils/logger';
 
 import { apiv3Get, apiv3Put } from '../client/util/apiv3-client';
@@ -14,8 +14,12 @@ const logger = loggerFactory('growi:stores:personal-settings');
 
 
 export const useSWRxPersonalSettings = (config?: SWRConfiguration): SWRResponse<IUser, Error> => {
+  const { data: isGuestUser } = useIsGuestUser();
+
+  const key = !isGuestUser ? '/personal-setting' : null;
+
   return useSWR(
-    '/personal-setting',
+    key,
     endpoint => apiv3Get(endpoint).then(response => response.data.currentUser),
     config,
   );

+ 6 - 2
packages/app/test/cypress/integration/20-basic-features/20-basic-features--click-page-icons.spec.ts

@@ -112,7 +112,9 @@ context('Click page icons button', () => {
       // do
       cy.get('#po-total-bookmarks').click({force: true});
       // wait until
-      return cy.get('.user-list-popover').then($elem => $elem.is(':visible'));
+      return cy.get('body').within(() => {
+        return Cypress.$('.user-list-popover').is(':visible');
+      });
     });
     cy.waitUntilSpinnerDisappear();
     cy.get('#grw-subnav-container').within(() => { cy.screenshot(`${ssPrefix}8-bookmarks-counter`) });
@@ -137,7 +139,9 @@ context('Click page icons button', () => {
       // do
       cy.get('#po-total-bookmarks').click({force: true});
       // wait until
-      return cy.get('.user-list-popover').then($elem => $elem.is(':visible'));
+      return cy.get('body').within(() => {
+        return Cypress.$('.user-list-popover').is(':visible');
+      });
     });
     cy.waitUntilSpinnerDisappear();
     cy.get('#grw-subnav-container').within(() => { cy.screenshot(`${ssPrefix}10-bookmarks-counter`) });

+ 10 - 3
packages/app/test/cypress/integration/20-basic-features/20-basic-features--use-tools.spec.ts

@@ -45,8 +45,13 @@ context('Modal for page operation', () => {
       cy.getByTestid('btn-create-memo').click();
     });
     cy.getByTestid('page-editor').should('be.visible');
-    cy.getByTestid('save-page-btn').click();
-    cy.get('.layout-root').should('not.have.class', 'editing');
+
+    cy.waitUntil(() => {
+      // do
+      cy.getByTestid('save-page-btn').should('be.visible').click();
+      // wait until
+      return cy.get('.layout-root').then($elem => $elem.hasClass('editing'));
+    });
 
     cy.getByTestid('grw-contextual-sub-nav').should('be.visible');
 
@@ -64,7 +69,9 @@ context('Modal for page operation', () => {
       // do
       cy.getByTestid('newPageBtn').click({force: true});
       // wait until
-      return cy.getByTestid('page-create-modal').then($elem => $elem.is(':visible'));
+      return cy.get('body').within(() => {
+        return Cypress.$('[data-testid=page-create-modal]').is(':visible');
+      });
     });
 
     cy.getByTestid('page-create-modal').should('be.visible').within(() => {