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

Merge branch 'support/create-UsersHomePageFooter-integrate' into feat/update-PageComment-for-SearchResultContent-page

jam411 3 лет назад
Родитель
Сommit
3c0e8936c3

+ 12 - 18
packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -43,6 +43,16 @@ import { SubNavButtonsProps } from './SubNavButtons';
 import PageEditorModeManagerStyles from './PageEditorModeManager.module.scss';
 
 
+const PageEditorModeManager = dynamic(
+  () => import('./PageEditorModeManager'),
+  { ssr: false, loading: () => <Skelton additionalClass={`${PageEditorModeManagerStyles['grw-page-editor-mode-manager-skelton']}`} /> },
+);
+const SubNavButtons = dynamic<SubNavButtonsProps>(
+  () => import('./SubNavButtons').then(mod => mod.SubNavButtons),
+  { ssr: false, loading: () => <Skelton additionalClass='btn-skelton py-2' /> },
+);
+
+
 type AdditionalMenuItemsProps = {
   pageId: string,
   revisionId: string,
@@ -156,15 +166,6 @@ type GrowiContextualSubNavigationProps = {
 
 const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps): JSX.Element => {
 
-  const PageEditorModeManager = dynamic(
-    () => import('./PageEditorModeManager'),
-    { ssr: false, loading: () => <Skelton additionalClass={`${PageEditorModeManagerStyles['grw-page-editor-mode-manager-skelton']}`} /> },
-  );
-  const SubNavButtons = dynamic<SubNavButtonsProps>(
-    () => import('./SubNavButtons').then(mod => mod.SubNavButtons),
-    { ssr: false, loading: () => <Skelton additionalClass='btn-skelton py-2' /> },
-  );
-
   const { data: currentPage, mutate: mutateCurrentPage } = useSWRxCurrentPage();
   const path = currentPage?.path;
 
@@ -353,15 +354,8 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
         )}
       </>
     );
-  }, [
-    currentPage, currentUser, pageId, revisionId, shareLinkId, path, editorMode,
-    isCompactMode, isViewMode, isSharedUser, isAbleToShowPageManagement, isAbleToShowPageEditorModeManager,
-    isLinkSharingDisabled, isGuestUser, isPageTemplateModalShown,
-    duplicateItemClickedHandler, renameItemClickedHandler, deleteItemClickedHandler,
-    PageEditorModeManager, SubNavButtons,
-    mutateEditorMode,
-    templateMenuItemClickHandler,
-  ]);
+  // eslint-disable-next-line max-len
+  }, [currentPage, currentUser, pageId, revisionId, shareLinkId, path, editorMode, isCompactMode, isViewMode, isSharedUser, isAbleToShowPageManagement, isAbleToShowPageEditorModeManager, isLinkSharingDisabled, isGuestUser, isPageTemplateModalShown, duplicateItemClickedHandler, renameItemClickedHandler, deleteItemClickedHandler, mutateEditorMode, templateMenuItemClickHandler]);
 
   if (currentPathname == null) {
     return <></>;

+ 1 - 0
packages/app/src/components/Page/RevisionRenderer.tsx

@@ -97,6 +97,7 @@ const RevisionRenderer = React.memo((props: Props): JSX.Element => {
 
   return (
     <ReactMarkdown
+      data-testid="wiki"
       {...rendererOptions}
       className={`wiki ${additionalClassName ?? ''}`}
     >

+ 5 - 3
packages/app/src/components/PageAlert/TrashPageAlert.tsx

@@ -34,13 +34,15 @@ export const TrashPageAlert = (): JSX.Element => {
   const { open: openDeleteModal } = usePageDeleteModal();
   const { open: openPutBackPageModal } = usePutBackPageModal();
 
+  if (!isTrashPage) {
+    return <></>;
+  }
+
+
   const lastUpdateUserName = pageData?.lastUpdateUser?.name;
   const deletedAt = pageData?.deletedAt ? format(new Date(pageData?.deletedAt), 'yyyy/MM/dd HH:mm') : '';
   const revisionId = pageData?.revision?._id;
 
-  if (!isTrashPage) {
-    return <></>;
-  }
 
   function openPutbackPageModalHandler() {
     if (pageId === undefined || pagePath === undefined) {

+ 7 - 8
packages/app/src/components/PageEditor.tsx

@@ -69,7 +69,7 @@ const PageEditor = React.memo((): JSX.Element => {
   const { data: isTextlintEnabled } = useIsTextlintEnabled();
   const { data: isIndentSizeForced } = useIsIndentSizeForced();
   const { data: indentSize, mutate: mutateCurrentIndentSize } = useCurrentIndentSize();
-  const { data: isEnabledUnsavedWarning, mutate: mutateIsEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
+  const { mutate: mutateIsEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
   const { data: isUploadableFile } = useIsUploadableFile();
   const { data: isUploadableImage } = useIsUploadableImage();
 
@@ -87,18 +87,17 @@ const PageEditor = React.memo((): JSX.Element => {
   const editorRef = useRef<EditorRef>(null);
   const previewRef = useRef<HTMLDivElement>(null);
 
-  const setMarkdownWithDebounce = useMemo(() => debounce(50, throttle(100, (value) => {
+  const setMarkdownWithDebounce = useMemo(() => debounce(100, throttle(150, (value: string, isClean: boolean) => {
     markdownToSave.current = value;
     setMarkdownToPreview(value);
+
     // Displays an unsaved warning alert
-    if (!isEnabledUnsavedWarning) {
-      mutateIsEnabledUnsavedWarning(true);
-    }
-  })), [isEnabledUnsavedWarning, mutateIsEnabledUnsavedWarning]);
+    mutateIsEnabledUnsavedWarning(!isClean);
+  })), [mutateIsEnabledUnsavedWarning]);
 
 
-  const markdownChangedHandler = useCallback((value: string): void => {
-    setMarkdownWithDebounce(value);
+  const markdownChangedHandler = useCallback((value: string, isClean: boolean): void => {
+    setMarkdownWithDebounce(value, isClean);
   }, [setMarkdownWithDebounce]);
 
   const save = useCallback(async(opts?: {overwriteScopesOfDescendants: boolean}) => {

+ 8 - 1
packages/app/src/components/PageEditor/CodeMirrorEditor.jsx

@@ -168,6 +168,9 @@ class CodeMirrorEditor extends AbstractEditor {
     // ensure to be able to resolve 'this' to use 'codemirror.commands.save'
     this.getCodeMirror().codeMirrorEditor = this;
 
+    // mark clean
+    this.getCodeMirror().getDoc().markClean();
+
     // fold drawio section
     this.foldDrawioSection();
 
@@ -244,6 +247,9 @@ class CodeMirrorEditor extends AbstractEditor {
    */
   setValue(newValue) {
     this.getCodeMirror().getDoc().setValue(newValue);
+
+    // mark clean
+    this.getCodeMirror().getDoc().markClean();
   }
 
   /**
@@ -564,7 +570,8 @@ class CodeMirrorEditor extends AbstractEditor {
 
   changeHandler(editor, data, value) {
     if (this.props.onChange != null) {
-      this.props.onChange(value);
+      const isClean = data.origin == null || editor.isClean();
+      this.props.onChange(value, isClean);
     }
 
     this.updateCheatsheetStates(null, value);

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

@@ -29,7 +29,7 @@ type EditorPropsType = {
   isUploadable?: boolean,
   isUploadableFile?: boolean,
   isTextlintEnabled?: boolean,
-  onChange?: (newValue: string) => void,
+  onChange?: (newValue: string, isClean?: boolean) => void,
   onUpload?: (file) => void,
   indentSize?: number,
   onScroll?: ({ line: number }) => void,

+ 1 - 2
packages/app/src/pages/[[...path]].page.tsx

@@ -35,7 +35,7 @@ import { IUserUISettings } from '~/interfaces/user-ui-settings';
 import { PageModel, PageDocument } from '~/server/models/page';
 import { PageRedirectModel } from '~/server/models/page-redirect';
 import { UserUISettingsModel } from '~/server/models/user-ui-settings';
-import { useSWRxCurrentPage, useSWRxIsGrantNormalized, useSWRxPageInfo } from '~/stores/page';
+import { useSWRxCurrentPage, useSWRxIsGrantNormalized } from '~/stores/page';
 import { useRedirectFrom } from '~/stores/page-redirect';
 import {
   usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser, useSidebarCollapsed, useCurrentSidebarContents, useCurrentProductNavWidth, useSelectedGrant,
@@ -242,7 +242,6 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
 
   useCurrentPageId(pageId);
   useSWRxCurrentPage(undefined, pageWithMeta?.data); // store initial data
-  useSWRxPageInfo(pageId, undefined, pageWithMeta?.meta); // store initial data
   useIsTrashPage(_isTrashPage(pagePath));
   useIsUserPage(isUserPage(pagePath));
   useIsNotCreatable(props.isForbidden || !isCreatablePage(pagePath)); // TODO: need to include props.isIdentical

+ 21 - 14
packages/app/src/server/service/global-notification/global-notification-mail.js

@@ -20,17 +20,17 @@ class GlobalNotificationMailService {
    * @memberof GlobalNotificationMailService
    *
    * @param {string} event event name triggered
-   * @param {string} path path triggered the event
+   * @param {import('~/server/models/page').PageDocument} page page triggered the event
    * @param {User} triggeredBy user who triggered the event
    * @param {{ comment: Comment, oldPath: string }} _ event specific vars
    */
-  async fire(event, path, triggeredBy, vars) {
+  async fire(event, page, triggeredBy, vars) {
     const { mailService } = this.crowi;
 
     const GlobalNotification = this.crowi.model('GlobalNotificationSetting');
-    const notifications = await GlobalNotification.findSettingByPathAndEvent(event, path, this.type);
+    const notifications = await GlobalNotification.findSettingByPathAndEvent(event, page.path, this.type);
 
-    const option = this.generateOption(event, path, triggeredBy, vars);
+    const option = this.generateOption(event, page, triggeredBy, vars);
 
     await Promise.all(notifications.map((notification) => {
       return mailService.send({ ...option, to: notification.toEmail });
@@ -43,38 +43,45 @@ class GlobalNotificationMailService {
    * @memberof GlobalNotificationMailService
    *
    * @param {string} event event name triggered
-   * @param {string} path path triggered the event
+   * @param {import('~/server/models/page').PageDocument} page path triggered the event
    * @param {User} triggeredBy user triggered the event
    * @param {{ comment: Comment, oldPath: string }} _ event specific vars
    *
    * @return  {{ subject: string, template: string, vars: object }}
    */
-  generateOption(event, path, triggeredBy, { comment, oldPath }) {
+  generateOption(event, page, triggeredBy, { comment, oldPath }) {
     const defaultLang = this.crowi.configManager.getConfig('crowi', 'app:globalLang');
     // validate for all events
-    if (event == null || path == null || triggeredBy == null) {
+    if (event == null || page == null || triggeredBy == null) {
       throw new Error(`invalid vars supplied to GlobalNotificationMailService.generateOption for event ${event}`);
     }
 
     const template = nodePath.join(this.crowi.localeDir, `${defaultLang}/notifications/${event}.txt`);
+
+    const path = page.path;
+    const appTitle = this.crowi.appService.getAppTitle();
+    const siteUrl = this.crowi.appService.getSiteUrl();
+    const pageUrl = new URL(page._id, siteUrl);
+
     let subject;
     let vars = {
-      appTitle: this.crowi.appService.getAppTitle(),
+      appTitle,
+      siteUrl,
       path,
       username: triggeredBy.username,
     };
 
     switch (event) {
       case this.event.PAGE_CREATE:
-        subject = `#${event} - ${triggeredBy.username} created ${path}`;
+        subject = `#${event} - ${triggeredBy.username} created ${path} at URL: ${pageUrl}`;
         break;
 
       case this.event.PAGE_EDIT:
-        subject = `#${event} - ${triggeredBy.username} edited ${path}`;
+        subject = `#${event} - ${triggeredBy.username} edited ${path} at URL: ${pageUrl}`;
         break;
 
       case this.event.PAGE_DELETE:
-        subject = `#${event} - ${triggeredBy.username} deleted ${path}`;
+        subject = `#${event} - ${triggeredBy.username} deleted ${path} at URL: ${pageUrl}`;
         break;
 
       case this.event.PAGE_MOVE:
@@ -83,7 +90,7 @@ class GlobalNotificationMailService {
           throw new Error(`invalid vars supplied to GlobalNotificationMailService.generateOption for event ${event}`);
         }
 
-        subject = `#${event} - ${triggeredBy.username} moved ${oldPath} to ${path}`;
+        subject = `#${event} - ${triggeredBy.username} moved ${oldPath} to ${path} at URL: ${pageUrl}`;
         vars = {
           ...vars,
           oldPath,
@@ -92,7 +99,7 @@ class GlobalNotificationMailService {
         break;
 
       case this.event.PAGE_LIKE:
-        subject = `#${event} - ${triggeredBy.username} liked ${path}`;
+        subject = `#${event} - ${triggeredBy.username} liked ${path} at URL: ${pageUrl}`;
         break;
 
       case this.event.COMMENT:
@@ -101,7 +108,7 @@ class GlobalNotificationMailService {
           throw new Error(`invalid vars supplied to GlobalNotificationMailService.generateOption for event ${event}`);
         }
 
-        subject = `#${event} - ${triggeredBy.username} commented on ${path}`;
+        subject = `#${event} - ${triggeredBy.username} commented on ${path} at URL: ${pageUrl}`;
         vars = {
           ...vars,
           comment: comment.comment,

+ 1 - 1
packages/app/src/server/service/global-notification/index.js

@@ -45,7 +45,7 @@ class GlobalNotificationService {
     }
 
     await Promise.all([
-      this.gloabalNotificationMail.fire(event, page.path, triggeredBy, vars),
+      this.gloabalNotificationMail.fire(event, page, triggeredBy, vars),
       this.gloabalNotificationSlack.fire(event, page.id, page.path, triggeredBy, vars),
     ]);
   }

+ 4 - 1
packages/app/src/styles/style-next.scss

@@ -17,9 +17,12 @@
 @import '~katex/dist/katex.min';
 
 // icons
+
+// DO NOT CHANGE THER OERDER OF font-awesome AND simple-line-icons.
+// font-familiy used in simple-line-icons has to be prioritized than the one used in font-awesome.
+@import '~font-awesome';
 @import '~simple-line-icons';
 @import '~material-icons/iconfont/filled';
-@import '~font-awesome';
 @import '~@icon/themify-icons/themify-icons';
 
 // atoms

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

@@ -218,7 +218,7 @@ context('Tag Oprations', () =>{
       cy.get('.modal-footer > button.btn').click();
     });
     cy.visit(`/${newPageName}`);
-    cy.get('#wiki').should('not.be.empty');
+    cy.getByTestid('wiki').should('exist');
     cy.screenshot(`${ssPrefix}4-duplicated-page`, {capture: 'viewport'});
   });
 

+ 0 - 109
packages/plugin-lsx/src/components/LsxPageList/LsxPage.jsx

@@ -1,109 +0,0 @@
-import React from 'react';
-
-import { pathUtils } from '@growi/core';
-import { PageListMeta } from '@growi/ui';
-import PropTypes from 'prop-types';
-
-import { PageNode } from '../PageNode';
-import { LsxContext } from '../lsx-context';
-
-import { PagePathWrapper } from './PagePathWrapper';
-
-export class LsxPage extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.state = {
-      isExists: false,
-      isLinkable: false,
-      hasChildren: false,
-    };
-  }
-
-  UNSAFE_componentWillMount() {
-    const pageNode = this.props.pageNode;
-
-    if (pageNode.page !== undefined) {
-      this.setState({ isExists: true });
-    }
-    if (pageNode.children.length > 0) {
-      this.setState({ hasChildren: true });
-    }
-
-    // process depth option
-    const optDepth = this.props.lsxContext.getOptDepth();
-    if (optDepth == null) {
-      this.setState({ isLinkable: true });
-    }
-    else {
-      const depth = this.props.depth;
-
-      // debug
-      // console.log(pageNode.pagePath, {depth, decGens, 'optDepth.start': optDepth.start, 'optDepth.end': optDepth.end});
-
-      const isLinkable = optDepth.start <= depth;
-      this.setState({ isLinkable });
-    }
-  }
-
-  getChildPageElement() {
-    const pageNode = this.props.pageNode;
-
-    let element = '';
-
-    // create child pages elements
-    if (this.state.hasChildren) {
-      const pages = pageNode.children.map((pageNode) => {
-        return (
-          <LsxPage
-            key={pageNode.pagePath}
-            depth={this.props.depth + 1}
-            pageNode={pageNode}
-            lsxContext={this.props.lsxContext}
-            basisViewersCount={this.props.basisViewersCount}
-          />
-        );
-      });
-
-      element = <ul className="page-list-ul">{pages}</ul>;
-    }
-
-    return element;
-  }
-
-  getIconElement() {
-    return (this.state.isExists)
-      ? <i className="ti ti-agenda" aria-hidden="true"></i>
-      : <i className="ti ti-file lsx-page-not-exist" aria-hidden="true"></i>;
-  }
-
-  render() {
-    const { pageNode, basisViewersCount } = this.props;
-
-    // create PagePath element
-    let pagePathNode = <PagePathWrapper pagePath={pageNode.pagePath} isExists={this.state.isExists} />;
-    if (this.state.isLinkable) {
-      pagePathNode = <a className="page-list-link" href={encodeURI(pathUtils.removeTrailingSlash(pageNode.pagePath))}>{pagePathNode}</a>;
-    }
-
-    // create PageListMeta element
-    const pageListMeta = (this.state.isExists) ? <PageListMeta page={pageNode.page} basisViewersCount={basisViewersCount} /> : '';
-
-    return (
-      <li className="page-list-li">
-        <small>{this.getIconElement()}</small> {pagePathNode}
-        <span className="ml-2">{pageListMeta}</span>
-        {this.getChildPageElement()}
-      </li>
-    );
-  }
-
-}
-
-LsxPage.propTypes = {
-  pageNode: PropTypes.instanceOf(PageNode).isRequired,
-  lsxContext: PropTypes.instanceOf(LsxContext).isRequired,
-  depth: PropTypes.number,
-  basisViewersCount: PropTypes.number,
-};

+ 95 - 0
packages/plugin-lsx/src/components/LsxPageList/LsxPage.tsx

@@ -0,0 +1,95 @@
+import React, { useMemo } from 'react';
+
+import { pathUtils } from '@growi/core';
+import { PagePathLabel, PageListMeta } from '@growi/ui';
+
+import { PageNode } from '../PageNode';
+import { LsxContext } from '../lsx-context';
+
+
+type Props = {
+  pageNode: PageNode,
+  lsxContext: LsxContext,
+  depth: number,
+  basisViewersCount?: number,
+};
+
+export const LsxPage = React.memo((props: Props): JSX.Element => {
+  const {
+    pageNode, lsxContext, depth, basisViewersCount,
+  } = props;
+
+  const isExists = pageNode.page !== undefined;
+  const isLinkable = (() => {
+    // process depth option
+    const optDepth = lsxContext.getOptDepth();
+    if (optDepth == null) {
+      return true;
+    }
+
+    // debug
+    // console.log(pageNode.pagePath, {depth, decGens, 'optDepth.start': optDepth.start, 'optDepth.end': optDepth.end});
+
+    return optDepth.start <= depth;
+  })();
+  const hasChildren = pageNode.children.length > 0;
+
+  const childrenElements: JSX.Element = useMemo(() => {
+    let element = <></>;
+
+    // create child pages elements
+    if (hasChildren) {
+      const pages = pageNode.children.map((pageNode) => {
+        return (
+          <LsxPage
+            key={pageNode.pagePath}
+            depth={depth + 1}
+            pageNode={pageNode}
+            lsxContext={lsxContext}
+            basisViewersCount={basisViewersCount}
+          />
+        );
+      });
+
+      element = <ul className="page-list-ul">{pages}</ul>;
+    }
+
+    return element;
+  }, [basisViewersCount, depth, hasChildren, lsxContext, pageNode.children]);
+
+  const iconElement: JSX.Element = useMemo(() => {
+    return (isExists)
+      ? <i className="ti ti-agenda" aria-hidden="true"></i>
+      : <i className="ti ti-file lsx-page-not-exist" aria-hidden="true"></i>;
+  }, [isExists]);
+
+  const pagePathElement: JSX.Element = useMemo(() => {
+    const classNames: string[] = [];
+    if (!isExists) {
+      classNames.push('lsx-page-not-exist');
+    }
+
+    // create PagePath element
+    let pagePathNode = <PagePathLabel path={pageNode.pagePath} isLatterOnly additionalClassNames={classNames} />;
+    if (isLinkable) {
+      pagePathNode = <a className="page-list-link" href={encodeURI(pathUtils.removeTrailingSlash(pageNode.pagePath))}>{pagePathNode}</a>;
+    }
+    return pagePathNode;
+  }, [isExists, isLinkable, pageNode.pagePath]);
+
+  const pageListMetaElement: JSX.Element = useMemo(() => {
+    if (!isExists) {
+      return <></>;
+    }
+    return <PageListMeta page={pageNode.page} basisViewersCount={basisViewersCount} />;
+  }, [basisViewersCount, isExists, pageNode.page]);
+
+  return (
+    <li className="page-list-li">
+      <small>{iconElement}</small> {pagePathElement}
+      <span className="ml-2">{pageListMetaElement}</span>
+      {childrenElements}
+    </li>
+  );
+
+});

+ 0 - 31
packages/plugin-lsx/src/components/LsxPageList/PagePathWrapper.jsx

@@ -1,31 +0,0 @@
-import React from 'react';
-
-import { PagePathLabel } from '@growi/ui';
-import PropTypes from 'prop-types';
-
-
-export class PagePathWrapper extends React.Component {
-
-  render() {
-
-    const classNames = [];
-    if (!this.props.isExists) {
-      classNames.push('lsx-page-not-exist');
-    }
-
-    return (
-      <PagePathLabel path={this.props.pagePath} isLatterOnly additionalClassNames={classNames} />
-    );
-  }
-
-}
-
-PagePathWrapper.propTypes = {
-  pagePath: PropTypes.string.isRequired,
-  isExists: PropTypes.bool.isRequired,
-  excludePathString: PropTypes.string,
-};
-
-PagePathWrapper.defaultProps = {
-  excludePathString: '',
-};