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

Merge branch 'master' into support/cypress-v12

Shun Miyazawa 3 лет назад
Родитель
Сommit
882a7dc796

+ 1 - 0
packages/app/package.json

@@ -165,6 +165,7 @@
     "react-multiline-clamp": "^2.0.0",
     "react-scroll": "^1.8.7",
     "react-syntax-highlighter": "^15.5.0",
+    "react-toastify": "^9.1.1",
     "react-use-ripple": "^1.5.2",
     "reactstrap": "^8.9.0",
     "reconnecting-websocket": "^4.4.0",

+ 6 - 52
packages/app/src/client/util/apiNotification.js

@@ -1,53 +1,7 @@
-// show API error/sucess toastr
+import { legacy } from './toastr';
 
-import * as toastr from 'toastr';
-import { toArrayIfNot } from '~/utils/array-utils';
-
-const toastrOption = {
-  error: {
-    closeButton: true,
-    progressBar: true,
-    newestOnTop: false,
-    showDuration: '100',
-    hideDuration: '100',
-    timeOut: '0',
-  },
-  success: {
-    closeButton: true,
-    progressBar: true,
-    newestOnTop: false,
-    showDuration: '100',
-    hideDuration: '100',
-    timeOut: '3000',
-  },
-  warning: {
-    closeButton: true,
-    progressBar: true,
-    newestOnTop: false,
-    showDuration: '100',
-    hideDuration: '100',
-    timeOut: '6000',
-  },
-};
-
-// accepts both a single error and an array of errors
-export const toastError = (err, header = 'Error', option = toastrOption.error) => {
-  const errs = toArrayIfNot(err);
-
-  if (err.length === 0) {
-    toastr.error('', header);
-  }
-
-  for (const err of errs) {
-    toastr.error(err.message || err, header, option);
-  }
-};
-
-// only accepts a single item
-export const toastSuccess = (body, header = 'Success', option = toastrOption.success) => {
-  toastr.success(body, header, option);
-};
-
-export const toastWarning = (body, header = 'Warning', option = toastrOption.warning) => {
-  toastr.warning(body, header, option);
-};
+// DEPRECATED -- 2022.12.07 Yuki Takei
+// Use methods from './toastr.ts' instead
+export const toastError = legacy.toastError;
+export const toastSuccess = legacy.toastSuccess;
+export const toastWarning = legacy.toastWarning;

+ 91 - 0
packages/app/src/client/util/toastr.ts

@@ -0,0 +1,91 @@
+import { toast, ToastContent, ToastOptions } from 'react-toastify';
+import * as toastrLegacy from 'toastr';
+
+import { toArrayIfNot } from '~/utils/array-utils';
+
+
+export const toastErrorOption: ToastOptions = {
+  autoClose: 0,
+  closeButton: true,
+};
+export const toastError = (err: string | Error | Error[], option: ToastOptions = toastErrorOption): void => {
+  const errs = toArrayIfNot(err);
+
+  if (errs.length === 0) {
+    return;
+  }
+
+  for (const err of errs) {
+    const message = (typeof err === 'string') ? err : err.message;
+    toast.error(message || err, option);
+  }
+};
+
+export const toastSuccessOption: ToastOptions = {
+  autoClose: 2000,
+  closeButton: true,
+};
+export const toastSuccess = (content: ToastContent, option: ToastOptions = toastSuccessOption): void => {
+  toast.success(content, option);
+};
+
+export const toastWarningOption: ToastOptions = {
+  autoClose: 5000,
+  closeButton: true,
+};
+export const toastWarning = (content: ToastContent, option: ToastOptions = toastWarningOption): void => {
+  toastrLegacy.warning(content, option);
+};
+
+
+const toastrLegacyOption = {
+  error: {
+    closeButton: true,
+    progressBar: true,
+    newestOnTop: false,
+    showDuration: '100',
+    hideDuration: '100',
+    timeOut: '0',
+  },
+  success: {
+    closeButton: true,
+    progressBar: true,
+    newestOnTop: false,
+    showDuration: '100',
+    hideDuration: '100',
+    timeOut: '3000',
+  },
+  warning: {
+    closeButton: true,
+    progressBar: true,
+    newestOnTop: false,
+    showDuration: '100',
+    hideDuration: '100',
+    timeOut: '6000',
+  },
+};
+
+export const legacy = {
+  // accepts both a single error and an array of errors
+  toastError: (err: string | Error | Error[], header = 'Error', option = toastrLegacyOption.error): void => {
+    const errs = toArrayIfNot(err);
+
+    if (errs.length === 0) {
+      toastrLegacy.error('', header);
+    }
+
+    for (const err of errs) {
+      const message = (typeof err === 'string') ? err : err.message;
+      toastrLegacy.error(message || err, header, option);
+    }
+  },
+
+  // only accepts a single item
+  toastSuccess: (body: string, header = 'Success', option = toastrLegacyOption.success): void => {
+    toastrLegacy.success(body, header, option);
+  },
+
+  toastWarning: (body: string, header = 'Warning', option = toastrLegacyOption.warning): void => {
+    toastrLegacy.warning(body, header, option);
+  },
+};

+ 4 - 1
packages/app/src/components/Admin/ElasticsearchManagement/ElasticsearchManagement.tsx

@@ -54,9 +54,12 @@ const ElasticsearchManagement = () => {
             setIsConfigured(false);
           }
         }
+        toastError(errors as Error[]);
+      }
+      else {
+        toastError(errors as Error);
       }
 
-      toastError(errors);
     }
     finally {
       setIsInitialized(true);

+ 2 - 0
packages/app/src/components/Layout/RawLayout.tsx

@@ -1,6 +1,7 @@
 import React, { ReactNode, useState } from 'react';
 
 import Head from 'next/head';
+import { ToastContainer } from 'react-toastify';
 import { useIsomorphicLayoutEffect } from 'usehooks-ts';
 
 import { useGrowiTheme } from '~/stores/context';
@@ -48,6 +49,7 @@ export const RawLayout = ({ children, title, className }: Props): JSX.Element =>
         <GrowiThemeProvider theme={growiTheme} colorScheme={colorScheme}>
           <div className={classNames.join(' ')} data-color-scheme={colorScheme}>
             {children}
+            <ToastContainer theme={colorScheme} />
           </div>
         </GrowiThemeProvider>
       </NextThemesProvider>

+ 9 - 11
packages/app/src/components/Page/RenderTagLabels.tsx

@@ -21,25 +21,23 @@ const RenderTagLabels = React.memo((props: RenderTagLabelsProps) => {
   }
 
   const isTagsEmpty = tags.length === 0;
-  const tagElements = tags.map((tag) => {
-    return (
-      <a key={tag} href={`/_search?q=tag:${tag}`} className="grw-tag-label badge badge-secondary mr-2">
-        {tag}
-      </a>
-    );
-  });
 
   return (
     <>
-      {tagElements}
-
+      {tags.map((tag) => {
+        return (
+          <a key={tag} href={`/_search?q=tag:${tag}`} className="grw-tag-label badge badge-secondary mr-2">
+            {tag}
+          </a>
+        );
+      })}
       <div id="edit-tags-btn-wrapper-for-tooltip">
         <a
-          className={`btn btn-link btn-edit-tags p-0 text-muted d-flex ${isTagsEmpty ? 'no-tags' : ''} ${isGuestUser ? 'disabled' : ''}`}
+          className={`btn btn-link btn-edit-tags text-muted p-0 d-flex align-items-center ${isTagsEmpty && 'no-tags'} ${isGuestUser && 'disabled'}`}
           onClick={openEditorHandler}
         >
           { isTagsEmpty && <>{ t('Add tags for this page') }</>}
-          <i className="ml-1 icon-plus"></i>
+          <i className={`icon-plus ${isTagsEmpty && 'ml-1'}`}/>
         </a>
       </div>
       {isGuestUser && (

+ 1 - 1
packages/app/src/components/Page/TagLabels.tsx

@@ -37,7 +37,7 @@ export const TagLabels:FC<Props> = (props: Props) => {
   return (
     <>
       <div className={`${styles['grw-tag-labels']} grw-tag-labels d-flex align-items-center`} data-testid="grw-tag-labels">
-        <i className="tag-icon icon-tag mr-2"></i>
+        <i className="tag-icon icon-tag mr-2"/>
         <RenderTagLabels
           tags={tags}
           openEditorModal={openEditorModal}

+ 4 - 2
packages/app/src/components/PageComment/CommentEditor.tsx

@@ -233,6 +233,8 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
     );
   }, []);
 
+  const onChangeHandler = useCallback((newValue: string) => setComment(newValue), []);
+
   const renderReady = () => {
     const commentPreview = getCommentHtml();
 
@@ -269,10 +271,10 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
             <TabPane tabId="comment_editor">
               <Editor
                 ref={editorRef}
-                value={comment}
+                value={commentBody ?? ''} // DO NOT use state
                 isUploadable={isUploadable}
                 isUploadableFile={isUploadableFile}
-                onChange={setComment}
+                onChange={onChangeHandler}
                 onUpload={uploadHandler}
                 onCtrlEnter={ctrlEnterHandler}
                 isComment

+ 2 - 1
packages/app/src/components/PageEditor.tsx

@@ -2,6 +2,7 @@ import React, {
   useCallback, useEffect, useMemo, useRef, useState,
 } from 'react';
 
+
 import EventEmitter from 'events';
 
 import {
@@ -13,8 +14,8 @@ import { useRouter } from 'next/router';
 import { throttle, debounce } from 'throttle-debounce';
 
 import { useSaveOrUpdate } from '~/client/services/page-operation';
-import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { apiGet, apiPostForm } from '~/client/util/apiv1-client';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 import { IEditorMethods } from '~/interfaces/editor-methods';
 import { OptionsToSave } from '~/interfaces/page-operation';
 import { SocketEventName } from '~/interfaces/websocket';

+ 2 - 2
packages/app/src/components/PageEditor/LinkEditModal.jsx

@@ -463,11 +463,11 @@ class LinkEditModal extends React.PureComponent {
 
 }
 
-const LinkEditModalFc = React.forwardRef((props, ref) => {
+const LinkEditModalFc = React.memo(React.forwardRef((props, ref) => {
   const { t } = useTranslation();
   const { data: currentPath } = useCurrentPagePath();
   return <LinkEditModal t={t} ref={ref} pagePath={currentPath} {...props} />;
-});
+}));
 
 LinkEditModal.propTypes = {
   t: PropTypes.func.isRequired,

+ 12 - 0
packages/app/src/pages/invited.page.tsx

@@ -1,6 +1,7 @@
 import React from 'react';
 
 import type { IUserHasId, IUser } from '@growi/core';
+import { USER_STATUS } from '@growi/core';
 import { NextPage, GetServerSideProps, GetServerSidePropsContext } from 'next';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import dynamic from 'next/dynamic';
@@ -76,6 +77,17 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
 
   if (user != null) {
     props.currentUser = user.toObject();
+
+    // Only invited user can access to /invited page
+    if (props.currentUser.status !== USER_STATUS.INVITED) {
+      return {
+        redirect: {
+          permanent: false,
+          destination: '/',
+        },
+      };
+    }
+
   }
 
   await injectServerConfigurations(context, props);

+ 10 - 5
packages/app/src/stores/page-listing.tsx

@@ -134,18 +134,17 @@ export const useSWRxPageInfoForList = (
 };
 
 export const usePageTreeTermManager = (isDisabled?: boolean) : SWRResponse<number, Error> & ITermNumberManagerUtil => {
-  return useTermNumberManager(isDisabled === true ? null : 'fullTextSearchTermNumber');
+  return useTermNumberManager(isDisabled === true ? null : 'pageTreeTermManager');
 };
 
 export const useSWRxRootPage = (): SWRResponse<RootPageResult, Error> => {
-  return useSWR(
+  return useSWRImmutable(
     '/page-listing/root',
     endpoint => apiv3Get(endpoint).then((response) => {
       return {
         rootPage: response.data.rootPage,
       };
     }),
-    { revalidateOnFocus: false },
   );
 };
 
@@ -154,14 +153,20 @@ export const useSWRxPageAncestorsChildren = (
 ): SWRResponse<AncestorsChildrenResult, Error> => {
   const { data: termNumber } = usePageTreeTermManager();
 
-  return useSWR(
+  // HACKME: Consider using global mutation from useSWRConfig and not to use term number -- 2022/12/08 @hakumizuki
+  const prevTermNumber = termNumber ? termNumber - 1 : 0;
+  const prevSWRRes = useSWRImmutable(path ? [`/page-listing/ancestors-children?path=${path}`, prevTermNumber] : null);
+
+  return useSWRImmutable(
     path ? [`/page-listing/ancestors-children?path=${path}`, termNumber] : null,
     endpoint => apiv3Get(endpoint).then((response) => {
       return {
         ancestorsChildren: response.data.ancestorsChildren,
       };
     }),
-    { revalidateOnFocus: false },
+    {
+      fallbackData: prevSWRRes.data, // avoid data to be undefined due to the termNumber to change
+    },
   );
 };
 

+ 9 - 3
packages/app/src/styles/_editor.scss

@@ -69,9 +69,6 @@
     display: none;
   }
 
-  .toast-top-right {
-    top: 64px;
-  }
 
   /*****************
    * Expand Editor
@@ -276,6 +273,15 @@
       vertical-align: unset;
     }
   }
+
+
+  /*****************
+   *     Toastr
+   *****************/
+  .Toastify .Toastify__toast-container {
+    top: 4.5em;
+  }
+
 }
 
 .layout-root.editing {

+ 1 - 0
packages/app/src/styles/molecules/toastr.scss

@@ -1,3 +1,4 @@
 :root {
   @import '~toastr/build/toastr';
 }
+@import '~react-toastify/scss/main';

+ 1 - 1
packages/app/src/utils/array-utils.ts

@@ -1,6 +1,6 @@
 // converts non-array item to array
 
-export const toArrayIfNot = <T = unknown>(item?: T): T[] => {
+export const toArrayIfNot = <T = unknown>(item?: T | T[]): T[] => {
   if (item == null) {
     return [];
   }

+ 7 - 6
packages/app/test/cypress/integration/30-search/search.spec.ts

@@ -202,6 +202,7 @@ context('Search all pages', () => {
 
   });
 
+  // TODO: fix this VRT
   it('Successfully order page search results by tag', () => {
     const tag = 'help';
 
@@ -225,8 +226,8 @@ context('Search all pages', () => {
     cy.getByTestid('search-result-base').should('be.visible');
     cy.getByTestid('search-result-list').should('be.visible');
     cy.getByTestid('search-result-content').should('be.visible');
-    cy.get('.wiki').should('be.visible');
-    cy.screenshot(`${ssPrefix}2-tag-order-by-relevance`);
+    // cy.get('.wiki').should('be.visible');
+    // cy.screenshot(`${ssPrefix}2-tag-order-by-relevance`);
 
     cy.get('.grw-search-page-nav').within(() => {
       cy.get('button.dropdown-toggle').first().click({force: true});
@@ -236,8 +237,8 @@ context('Search all pages', () => {
     cy.getByTestid('search-result-base').should('be.visible');
     cy.getByTestid('search-result-list').should('be.visible');
     cy.getByTestid('search-result-content').should('be.visible');
-    cy.get('.wiki').should('be.visible');
-    cy.screenshot(`${ssPrefix}3-tag-order-by-creation-date`);
+    // cy.get('.wiki').should('be.visible');
+    // cy.screenshot(`${ssPrefix}3-tag-order-by-creation-date`);
 
     cy.get('.grw-search-page-nav').within(() => {
       cy.get('button.dropdown-toggle').first().click({force: true});
@@ -247,10 +248,10 @@ context('Search all pages', () => {
     cy.getByTestid('search-result-base').should('be.visible');
     cy.getByTestid('search-result-list').should('be.visible');
     cy.getByTestid('search-result-content').should('be.visible');
-    cy.get('.wiki').should('be.visible');
+    // cy.get('.wiki').should('be.visible');
     cy.waitUntilSpinnerDisappear();
 
-    cy.screenshot(`${ssPrefix}4-tag-order-by-last-update-date`);
+    // cy.screenshot(`${ssPrefix}4-tag-order-by-last-update-date`);
   });
 
 });

+ 12 - 0
yarn.lock

@@ -7001,6 +7001,11 @@ clsx@^1.0.4:
   resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.0.4.tgz#0c0171f6d5cb2fe83848463c15fcc26b4df8c2ec"
   integrity sha512-1mQ557MIZTrL/140j+JVdRM6e31/OA4vTYxXgqIIZlndyfjHpyawKZia1Im05Vp9BWmImkcNrNtFYQMyFcgJDg==
 
+clsx@^1.1.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
+  integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==
+
 cmd-shim@^4.1.0:
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/cmd-shim/-/cmd-shim-4.1.0.tgz#b3a904a6743e9fede4148c6f3800bf2a08135bdd"
@@ -18663,6 +18668,13 @@ react-syntax-highlighter@^15.5.0:
     prismjs "^1.27.0"
     refractor "^3.6.0"
 
+react-toastify@^9.1.1:
+  version "9.1.1"
+  resolved "https://registry.yarnpkg.com/react-toastify/-/react-toastify-9.1.1.tgz#9280caea4a13dc1739c350d90660a630807bf10b"
+  integrity sha512-pkFCla1z3ve045qvjEmn2xOJOy4ZciwRXm1oMPULVkELi5aJdHCN/FHnuqXq8IwGDLB7PPk2/J6uP9D8ejuiRw==
+  dependencies:
+    clsx "^1.1.1"
+
 react-transition-group@^2.3.1:
   version "2.9.0"
   resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.9.0.tgz#df9cdb025796211151a436c69a8f3b97b5b07c8d"