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

Merge branch 'master' into support/108836-refactor-common-process-in-page

yuken 3 лет назад
Родитель
Сommit
cad4120c0e
30 измененных файлов с 189 добавлено и 239 удалено
  1. 5 2
      .github/ISSUE_TEMPLATE/config.yml
  2. 0 25
      .github/ISSUE_TEMPLATE/user-request.md
  3. 0 5
      packages/app/src/client/util/smooth-scroll.ts
  4. 52 23
      packages/app/src/components/Comments.tsx
  5. 2 5
      packages/app/src/components/ContentLinkButtons.tsx
  6. 1 2
      packages/app/src/components/Fab.tsx
  7. 29 2
      packages/app/src/components/Page/PageView.tsx
  8. 3 2
      packages/app/src/components/PageComment/Comment.module.scss
  9. 8 0
      packages/app/src/components/PageDeleteModal.tsx
  10. 14 4
      packages/app/src/components/PageEditor.tsx
  11. 1 2
      packages/app/src/components/PageSideContents.tsx
  12. 14 1
      packages/app/src/components/ReactMarkdownComponents/Header.tsx
  13. 1 8
      packages/app/src/components/ReactMarkdownComponents/NextLink.tsx
  14. 10 10
      packages/app/src/components/SearchPage/SearchResultContent.tsx
  15. 5 5
      packages/app/src/components/Sidebar/PageTree/ItemsTree.tsx
  16. 15 5
      packages/app/src/server/routes/apiv3/users.js
  17. 4 1
      packages/app/src/services/renderer/rehype-plugins/relative-links.ts
  18. 5 0
      packages/app/src/services/renderer/renderer.tsx
  19. 3 13
      packages/app/src/stores/renderer.tsx
  20. 0 3
      packages/app/src/styles/_attachments.scss
  21. 0 27
      packages/app/src/styles/_comment.scss
  22. 0 20
      packages/app/src/styles/_comment_growi.scss
  23. 0 6
      packages/app/src/styles/_me.scss
  24. 0 9
      packages/app/src/styles/_old-ios.scss
  25. 0 9
      packages/app/src/styles/_page-duplicate-modal.scss
  26. 0 20
      packages/app/src/styles/_user.scss
  27. 2 0
      packages/app/src/styles/_variables.scss
  28. 2 0
      packages/app/src/styles/organisms/_wiki.scss
  29. 0 7
      packages/app/src/styles/style-app.scss
  30. 13 23
      yarn.lock

+ 5 - 2
.github/ISSUE_TEMPLATE/config.yml

@@ -1,5 +1,8 @@
 blank_issues_enabled: false
 contact_links:
-  - name: Question or Suggestions
+  - name: User request or Suggestions
+    url: https://github.com/weseek/growi/discussions
+    about: If you have feature requests or suggestions, you can create a new discussion and consider it with the community.
+  - name: Questions
     url: https://growi-slackin.weseek.co.jp/
-    about: If you have questions or suggestions, you can join our Slack team and talk about anything, anytime.
+    about: If you have questions, you can join our Slack team and talk about anything, anytime.

+ 0 - 25
.github/ISSUE_TEMPLATE/user-request.md

@@ -1,25 +0,0 @@
----
-name: User request
-about: Suggest an idea for this project
-title: 'Request:'
-labels: user requests
----
-
-
-## Informations
-**Is your feature request related to a problem? Please describe.**
-A clear and concise description of what the problem is. 
-
-e.g. I'm having trouble getting immediate access to information
-
-**Describe the solution you'd like**
-A clear and concise description of what you want to realization.
-
-e.g. It's good if there is a space where everyone can access information in common.
-
-**Describe alternatives you've considered**
-A clear and concise description of any alternative solutions or features you've considered.
-
-- [ ] Custom Page In Sidebar
-  - [ ] Can be edited in `/Sidebar` path
-  - [ ] Can be described with md like a page

+ 0 - 5
packages/app/src/client/util/smooth-scroll.ts

@@ -1,5 +0,0 @@
-// option object for react-scroll
-export const DEFAULT_AUTO_SCROLL_OPTS = {
-  smooth: 'easeOutQuint',
-  duration: 1200,
-};

+ 52 - 23
packages/app/src/components/Comments.tsx

@@ -1,9 +1,9 @@
-import React from 'react';
+import React, { useEffect, useRef } from 'react';
 
 import { type IRevisionHasId, pagePathUtils } from '@growi/core';
 import dynamic from 'next/dynamic';
 
-import type { PageCommentProps } from '~/components/PageComment';
+import { ROOT_ELEM_ID as PageCommentRootElemId, type PageCommentProps } from '~/components/PageComment';
 import { useSWRxPageComment } from '~/stores/comment';
 import { useIsTrashPage } from '~/stores/page';
 
@@ -22,16 +22,47 @@ export type CommentsProps = {
   pageId: string,
   pagePath: string,
   revision: IRevisionHasId,
+  onLoaded?: () => void,
 }
 
 export const Comments = (props: CommentsProps): JSX.Element => {
 
-  const { pageId, pagePath, revision } = props;
+  const {
+    pageId, pagePath, revision, onLoaded,
+  } = props;
 
   const { mutate } = useSWRxPageComment(pageId);
   const { data: isDeleted } = useIsTrashPage();
   const { data: currentUser } = useCurrentUser();
 
+  const pageCommentParentRef = useRef<HTMLDivElement>(null);
+
+  useEffect(() => {
+    const parent = pageCommentParentRef.current;
+    if (parent == null) return;
+
+    const observerCallback = (mutationRecords:MutationRecord[]) => {
+      mutationRecords.forEach((record:MutationRecord) => {
+        const target = record.target as HTMLElement;
+
+        for (const child of Array.from(target.children)) {
+          const childId = (child as HTMLElement).id;
+          if (childId === PageCommentRootElemId) {
+            onLoaded?.();
+            break;
+          }
+        }
+
+      });
+    };
+
+    const observer = new MutationObserver(observerCallback);
+    observer.observe(parent, { childList: true });
+    return () => {
+      observer.disconnect();
+    };
+  }, [onLoaded]);
+
   const isTopPagePath = isTopPage(pagePath);
 
   if (pageId == null || isTopPagePath) {
@@ -41,29 +72,27 @@ export const Comments = (props: CommentsProps): JSX.Element => {
   return (
     <div className="page-comments-row mt-5 py-4 d-edit-none d-print-none">
       <div className="container-lg">
-        <div className="page-comments">
-          <div id="page-comments-list" className="page-comments-list">
-            <PageComment
+        <div id="page-comments-list" className="page-comments-list" ref={pageCommentParentRef}>
+          <PageComment
+            pageId={pageId}
+            pagePath={pagePath}
+            revision={revision}
+            currentUser={currentUser}
+            isReadOnly={false}
+            titleAlign="left"
+            hideIfEmpty={false}
+          />
+        </div>
+        { !isDeleted && (
+          <div id="page-comment-write">
+            <CommentEditor
               pageId={pageId}
-              pagePath={pagePath}
-              revision={revision}
-              currentUser={currentUser}
-              isReadOnly={false}
-              titleAlign="left"
-              hideIfEmpty={false}
+              isForNewComment
+              onCommentButtonClicked={mutate}
+              revisionId={revision._id}
             />
           </div>
-          { !isDeleted && (
-            <div id="page-comment-write">
-              <CommentEditor
-                pageId={pageId}
-                isForNewComment
-                onCommentButtonClicked={mutate}
-                revisionId={revision._id}
-              />
-            </div>
-          )}
-        </div>
+        )}
       </div>
     </div>
   );

+ 2 - 5
packages/app/src/components/ContentLinkButtons.tsx

@@ -3,17 +3,14 @@ import React from 'react';
 import { IUserHasId } from '@growi/core';
 import { Link as ScrollLink } from 'react-scroll';
 
-import { DEFAULT_AUTO_SCROLL_OPTS } from '~/client/util/smooth-scroll';
 import { RecentlyCreatedIcon } from '~/components/Icons/RecentlyCreatedIcon';
 
 import styles from './ContentLinkButtons.module.scss';
 
-const OFFSET = -120;
-
 const BookMarkLinkButton = React.memo(() => {
 
   return (
-    <ScrollLink to="bookmarks-list" offset={OFFSET} {...DEFAULT_AUTO_SCROLL_OPTS}>
+    <ScrollLink to="bookmarks-list" offset={-120}>
       <button
         type="button"
         className="btn btn-outline-secondary btn-sm px-2"
@@ -30,7 +27,7 @@ BookMarkLinkButton.displayName = 'BookMarkLinkButton';
 const RecentlyCreatedLinkButton = React.memo(() => {
 
   return (
-    <ScrollLink to="recently-created-list" offset={OFFSET} {...DEFAULT_AUTO_SCROLL_OPTS}>
+    <ScrollLink to="recently-created-list" offset={-120}>
       <button
         type="button"
         className="btn btn-outline-secondary btn-sm px-3"

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

@@ -6,7 +6,6 @@ import { animateScroll } from 'react-scroll';
 import { useRipple } from 'react-use-ripple';
 import StickyEvents from 'sticky-events';
 
-import { DEFAULT_AUTO_SCROLL_OPTS } from '~/client/util/smooth-scroll';
 import { usePageCreateModal } from '~/stores/modal';
 import { useCurrentPagePath } from '~/stores/page';
 import { useIsAbleToChangeEditorMode } from '~/stores/ui';
@@ -96,7 +95,7 @@ export const Fab = (): JSX.Element => {
 
   const ScrollToTopButton = useCallback(() => {
     const clickHandler = () => {
-      animateScroll.scrollToTop(DEFAULT_AUTO_SCROLL_OPTS);
+      animateScroll.scrollToTop({ duration: 200 });
     };
 
     return (

+ 29 - 2
packages/app/src/components/Page/PageView.tsx

@@ -1,4 +1,7 @@
-import React, { useEffect, useMemo } from 'react';
+import React, {
+  useEffect, useMemo, useRef, useState,
+} from 'react';
+
 
 import { type IPagePopulatedToShowRevision, pagePathUtils } from '@growi/core';
 import dynamic from 'next/dynamic';
@@ -13,6 +16,7 @@ import { useViewOptions } from '~/stores/renderer';
 import { useIsMobile } from '~/stores/ui';
 import { registerGrowiFacade } from '~/utils/growi-facade';
 
+
 import type { CommentsProps } from '../Comments';
 import { MainPane } from '../Layout/MainPane';
 import { PageAlerts } from '../PageAlert/PageAlerts';
@@ -47,6 +51,11 @@ type Props = {
 }
 
 export const PageView = (props: Props): JSX.Element => {
+
+  const commentsContainerRef = useRef<HTMLDivElement>(null);
+
+  const [isCommentsLoaded, setCommentsLoaded] = useState(false);
+
   const {
     pagePath, initialPage, rendererConfig,
   } = props;
@@ -75,6 +84,22 @@ export const PageView = (props: Props): JSX.Element => {
     });
   }, [mutateRendererOptions]);
 
+  // ***************************  Auto Scroll  ***************************
+  useEffect(() => {
+    // do nothing if hash is empty
+    const { hash } = window.location;
+    if (hash.length === 0) {
+      return;
+    }
+
+    const targetId = hash.slice(1);
+
+    const target = document.getElementById(targetId);
+    target?.scrollIntoView();
+
+  }, [isCommentsLoaded]);
+  // *******************************  end  *******************************
+
   const specialContents = useMemo(() => {
     if (isIdenticalPathPage) {
       return <IdenticalPathPage />;
@@ -96,7 +121,9 @@ export const PageView = (props: Props): JSX.Element => {
   const footerContents = !isIdenticalPathPage && !isNotFound
     ? (
       <>
-        <Comments pageId={page._id} pagePath={pagePath} revision={page.revision} />
+        <div id="comments-container" ref={commentsContainerRef}>
+          <Comments pageId={page._id} pagePath={pagePath} revision={page.revision} onLoaded={() => setCommentsLoaded(true)} />
+        </div>
         { isUsersHomePagePath && (
           <UsersHomePageFooter creatorId={page.creator._id}/>
         ) }

+ 3 - 2
packages/app/src/components/PageComment/Comment.module.scss

@@ -1,3 +1,4 @@
+@use '../../styles/variables' as var;
 @use '../../styles/bootstrap/init' as bs;
 @use './_comment-inheritance';
 
@@ -10,10 +11,10 @@
 
   .page-comment {
     position: relative;
-    padding-top: 70px;
-    margin-top: -70px;
     pointer-events: none;
 
+    scroll-margin-top: var.$grw-scroll-margin-top-in-view;
+
     // user name
     .page-comment-creator {
       margin-top: -0.5em;

+ 8 - 0
packages/app/src/components/PageDeleteModal.tsx

@@ -83,6 +83,14 @@ const PageDeleteModal: FC = () => {
   // eslint-disable-next-line @typescript-eslint/no-unused-vars
   const [errs, setErrs] = useState<Error[] | null>(null);
 
+  // initialize when opening modal
+  useEffect(() => {
+    if (isOpened) {
+      setIsDeleteRecursively(true);
+      setIsDeleteCompletely(forceDeleteCompletelyMode);
+    }
+  }, [forceDeleteCompletelyMode, isOpened]);
+
   useEffect(() => {
     setIsDeleteCompletely(forceDeleteCompletelyMode);
   }, [forceDeleteCompletelyMode]);

+ 14 - 4
packages/app/src/components/PageEditor.tsx

@@ -16,7 +16,7 @@ import { throttle, debounce } from 'throttle-debounce';
 
 import { useUpdateStateAfterSave, useSaveOrUpdate } from '~/client/services/page-operation';
 import { apiGet, apiPostForm } from '~/client/util/apiv1-client';
-import { toastError, toastSuccess } from '~/client/util/toastr';
+import { toastError, toastSuccess, toastWarning } from '~/client/util/toastr';
 import { IEditorMethods } from '~/interfaces/editor-methods';
 import { OptionsToSave } from '~/interfaces/page-operation';
 import { SocketEventName } from '~/interfaces/websocket';
@@ -218,6 +218,7 @@ const PageEditor = React.memo((): JSX.Element => {
       logger.error('failed to save', error);
       toastError(error);
       if (error.code === 'conflict') {
+        toastWarning('(TBD) resolve conflict');
         // pageContainer.setState({
         //   remoteRevisionId: error.data.revisionId,
         //   remoteRevisionBody: error.data.revisionBody,
@@ -255,11 +256,20 @@ const PageEditor = React.memo((): JSX.Element => {
     }
 
     const page = await save();
-    if (page != null) {
+    if (page == null) {
+      return;
+    }
+
+    if (isNotFound) {
+      await router.push(`/${page._id}#edit`);
+    }
+    else {
       updateStateAfterSave?.();
-      toastSuccess(t('toaster.save_succeeded'));
     }
-  }, [editorMode, save, t, updateStateAfterSave]);
+    toastSuccess(t('toaster.save_succeeded'));
+    mutateEditorMode(EditorMode.Editor);
+
+  }, [editorMode, isNotFound, mutateEditorMode, router, save, t, updateStateAfterSave]);
 
 
   /**

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

@@ -4,7 +4,6 @@ import { IPageHasId, pagePathUtils } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import { Link } from 'react-scroll';
 
-import { DEFAULT_AUTO_SCROLL_OPTS } from '~/client/util/smooth-scroll';
 import { useDescendantsPageListModal } from '~/stores/modal';
 
 import CountBadge from './Common/CountBadge';
@@ -57,7 +56,7 @@ export const PageSideContents = (props: PageSideContentsProps): JSX.Element => {
       {/* Comments */}
       { !isTopPagePath && (
         <div className={`mt-2 grw-page-accessories-control ${styles['grw-page-accessories-control']}`}>
-          <Link to={'page-comments'} offset={-100} {...DEFAULT_AUTO_SCROLL_OPTS}>
+          <Link to={'page-comments'} offset={-120}>
             <button
               type="button"
               className="btn btn-block btn-outline-secondary grw-btn-page-accessories rounded-pill d-flex justify-content-between align-items-center"

+ 14 - 1
packages/app/src/components/ReactMarkdownComponents/Header.tsx

@@ -85,7 +85,7 @@ export const Header = (props: HeaderProps): JSX.Element => {
     activateByHash(window.location.href);
   }, [activateByHash]);
 
-  // update isActive when hash is changed
+  // update isActive when hash is changed by next router
   useEffect(() => {
     router.events.on('hashChangeComplete', activateByHash);
 
@@ -94,6 +94,19 @@ export const Header = (props: HeaderProps): JSX.Element => {
     };
   }, [activateByHash, router.events]);
 
+  // update isActive when hash is changed
+  useEffect(() => {
+    const activeByHashWrapper = (e: HashChangeEvent) => {
+      activateByHash(e.newURL);
+    };
+
+    window.addEventListener('hashchange', activeByHashWrapper);
+
+    return () => {
+      window.removeEventListener('hashchange', activeByHashWrapper);
+    };
+  }, [activateByHash, router.events]);
+
   const showEditButton = !isGuestUser && !isSharedUser && shareLinkId == null;
 
   return (

+ 1 - 8
packages/app/src/components/ReactMarkdownComponents/NextLink.tsx

@@ -1,7 +1,5 @@
 import Link, { LinkProps } from 'next/link';
-import { Link as ScrollLink } from 'react-scroll';
 
-import { DEFAULT_AUTO_SCROLL_OPTS } from '~/client/util/smooth-scroll';
 import { useSiteUrl } from '~/stores/context';
 import loggerFactory from '~/utils/logger';
 
@@ -42,13 +40,8 @@ export const NextLink = ({
 
   // when href is an anchor link
   if (isAnchorLink(href)) {
-    const to = href.slice(1);
     return (
-      <Link href={href} scroll={false}>
-        <ScrollLink href={href} to={to} className={className} offset={-100} {...DEFAULT_AUTO_SCROLL_OPTS}>
-          {children}
-        </ScrollLink>
-      </Link>
+      <a href={href} className={className}>{children}</a>
     );
   }
 

+ 10 - 10
packages/app/src/components/SearchPage/SearchResultContent.tsx

@@ -11,9 +11,9 @@ import { DropdownItem } from 'reactstrap';
 
 import { exportAsMarkdown, updateContentWidth } from '~/client/services/page-operation';
 import { toastSuccess } from '~/client/util/apiNotification';
-import { IPageToDeleteWithMeta, IPageToRenameWithMeta } from '~/interfaces/page';
-import { IPageWithSearchMeta } from '~/interfaces/search';
-import { OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction } from '~/interfaces/ui';
+import type { IPageToDeleteWithMeta, IPageToRenameWithMeta } from '~/interfaces/page';
+import type { IPageWithSearchMeta } from '~/interfaces/search';
+import type { OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction } from '~/interfaces/ui';
 import { useCurrentUser, useIsContainerFluid } from '~/stores/context';
 import {
   usePageDuplicateModal, usePageRenameModal, usePageDeleteModal,
@@ -22,12 +22,12 @@ import { mutatePageList, mutatePageTree } from '~/stores/page-listing';
 import { useSearchResultOptions } from '~/stores/renderer';
 import { mutateSearching } from '~/stores/search';
 
-import { AdditionalMenuItemsRendererProps, ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
-import { GrowiSubNavigationProps } from '../Navbar/GrowiSubNavigation';
-import { SubNavButtonsProps } from '../Navbar/SubNavButtons';
-import { ROOT_ELEM_ID as RevisionLoaderRoomElemId, RevisionLoaderProps } from '../Page/RevisionLoader';
-import { ROOT_ELEM_ID as PageCommentRootElemId, PageCommentProps } from '../PageComment';
-import { PageContentFooterProps } from '../PageContentFooter';
+import type { AdditionalMenuItemsRendererProps, ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
+import type { GrowiSubNavigationProps } from '../Navbar/GrowiSubNavigation';
+import type { SubNavButtonsProps } from '../Navbar/SubNavButtons';
+import { ROOT_ELEM_ID as RevisionLoaderRoomElemId, type RevisionLoaderProps } from '../Page/RevisionLoader';
+import { ROOT_ELEM_ID as PageCommentRootElemId, type PageCommentProps } from '../PageComment';
+import type { PageContentFooterProps } from '../PageContentFooter';
 
 import styles from './SearchResultContent.module.scss';
 
@@ -96,7 +96,7 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
     const scrollElement = scrollElementRef.current;
     if (scrollElement == null) return;
 
-    const observerCallback = (mutationRecords:MutationRecord[], thisObs: MutationObserver) => {
+    const observerCallback = (mutationRecords:MutationRecord[]) => {
       mutationRecords.forEach((record:MutationRecord) => {
         const target = record.target as HTMLElement;
 

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

@@ -2,6 +2,8 @@ import React, {
   useEffect, useRef, useState, useMemo, useCallback,
 } from 'react';
 
+import path from 'path';
+
 import { Nullable } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import { useRouter } from 'next/router';
@@ -175,13 +177,11 @@ const ItemsTree = (props: ItemsTreeProps): JSX.Element => {
         return;
       }
 
-      const path = pathOrPathsToDelete;
-
       if (isCompletely) {
-        toastSuccess(t('deleted_pages_completely', { path }));
+        toastSuccess(t('deleted_pages_completely', { path: pathOrPathsToDelete }));
       }
       else {
-        toastSuccess(t('deleted_pages', { path }));
+        toastSuccess(t('deleted_pages', { path: pathOrPathsToDelete }));
       }
 
       mutatePageTree();
@@ -191,7 +191,7 @@ const ItemsTree = (props: ItemsTreeProps): JSX.Element => {
 
       if (currentPagePath === pathOrPathsToDelete) {
         mutateCurrentPage();
-        router.push(`/trash${pathOrPathsToDelete}`);
+        router.push(isCompletely ? path.dirname(pathOrPathsToDelete) : `/trash${pathOrPathsToDelete}`);
       }
     };
 

+ 15 - 5
packages/app/src/server/routes/apiv3/users.js

@@ -484,9 +484,11 @@ module.exports = (crowi) => {
       const userData = await User.findById(id);
       await userData.makeAdmin();
 
+      const serializedUserData = serializeUserSecurely(userData);
+
       activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_USERS_GIVE_ADMIN });
 
-      return res.apiv3({ userData });
+      return res.apiv3({ userData: serializedUserData });
     }
     catch (err) {
       logger.error('Error', err);
@@ -529,9 +531,11 @@ module.exports = (crowi) => {
       const userData = await User.findById(id);
       await userData.removeFromAdmin();
 
+      const serializedUserData = serializeUserSecurely(userData);
+
       activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_USERS_REMOVE_ADMIN });
 
-      return res.apiv3({ userData });
+      return res.apiv3({ userData: serializedUserData });
     }
     catch (err) {
       logger.error('Error', err);
@@ -581,9 +585,11 @@ module.exports = (crowi) => {
       const userData = await User.findById(id);
       await userData.statusActivate();
 
+      const serializedUserData = serializeUserSecurely(userData);
+
       activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_USERS_ACTIVATE });
 
-      return res.apiv3({ userData });
+      return res.apiv3({ userData: serializedUserData });
     }
     catch (err) {
       logger.error('Error', err);
@@ -625,9 +631,11 @@ module.exports = (crowi) => {
       const userData = await User.findById(id);
       await userData.statusSuspend();
 
+      const serializedUserData = serializeUserSecurely(userData);
+
       activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_USERS_DEACTIVATE });
 
-      return res.apiv3({ userData });
+      return res.apiv3({ userData: serializedUserData });
     }
     catch (err) {
       logger.error('Error', err);
@@ -672,9 +680,11 @@ module.exports = (crowi) => {
       await ExternalAccount.remove({ user: userData });
       await Page.removeByPath(`/user/${userData.username}`);
 
+      const serializedUserData = serializeUserSecurely(userData);
+
       activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_USERS_REMOVE });
 
-      return res.apiv3({ userData });
+      return res.apiv3({ userData: serializedUserData });
     }
     catch (err) {
       logger.error('Error', err);

+ 4 - 1
packages/app/src/services/renderer/rehype-plugins/relative-links.ts

@@ -17,6 +17,9 @@ const defaultHrefResolver: IHrefResolver = (relativeHref, basePath) => {
   return relativeUrl.pathname;
 };
 
+const isAnchorLink = (href: string): boolean => {
+  return href.toString().length > 0 && href[0] === '#';
+};
 
 export type RelativeLinksPluginParams = {
   pagePath?: string,
@@ -42,7 +45,7 @@ export const relativeLinks: Plugin<[RelativeLinksPluginParams]> = (options = {})
       }
 
       const href = anchor.properties.href;
-      if (href == null || typeof href !== 'string' || isAbsolute(href)) {
+      if (href == null || typeof href !== 'string' || isAbsolute(href) || isAnchorLink(href)) {
         return;
       }
 

+ 5 - 0
packages/app/src/services/renderer/renderer.tsx

@@ -122,6 +122,10 @@ const generateCommonOptions = (pagePath: string|undefined): RendererOptions => {
       pukiwikiLikeLinker,
       growiDirective,
     ],
+    remarkRehypeOptions: {
+      clobberPrefix: 'mdcont-',
+      allowDangerousHtml: true,
+    },
     rehypePlugins: [
       [relativeLinksByPukiwikiLikeLinker, { pagePath }],
       [relativeLinks, { pagePath }],
@@ -324,6 +328,7 @@ export const generateSSRViewOptions = (
 
   // add rehype plugins
   rehypePlugins.push(
+    slug,
     [lsxGrowiPlugin.rehypePlugin, { pagePath, isSharedPage: config.isSharedPage }],
     rehypeSanitizePlugin,
     katex,

+ 3 - 13
packages/app/src/stores/renderer.tsx

@@ -1,6 +1,4 @@
-import {
-  useCallback, useEffect, useRef,
-} from 'react';
+import { useCallback } from 'react';
 
 import type { HtmlElementNode } from 'rehype-toc';
 import useSWR, { type SWRResponse } from 'swr';
@@ -25,17 +23,9 @@ export const useViewOptions = (): SWRResponse<RendererOptions, Error> => {
   const { data: rendererConfig } = useRendererConfig();
   const { mutate: mutateCurrentPageTocNode } = useCurrentPageTocNode();
 
-  const tocRef = useRef<HtmlElementNode|undefined>();
-
   const storeTocNodeHandler = useCallback((toc: HtmlElementNode) => {
-    tocRef.current = toc;
-  }, []);
-
-  useEffect(() => {
-    mutateCurrentPageTocNode(tocRef.current);
-  // using useRef not to re-render
-  // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [mutateCurrentPageTocNode, tocRef.current]);
+    mutateCurrentPageTocNode(toc, { revalidate: false });
+  }, [mutateCurrentPageTocNode]);
 
   const isAllDataValid = currentPagePath != null && rendererConfig != null;
 

+ 0 - 3
packages/app/src/styles/_attachments.scss

@@ -1,3 +0,0 @@
-.attachment-userpicture .picture {
-  vertical-align: text-bottom;
-}

+ 0 - 27
packages/app/src/styles/_comment.scss

@@ -1,27 +0,0 @@
-// modal
-.page-comment-delete-modal .modal-content {
-  .modal-body {
-    .comment-body {
-      max-height: 13em;
-      // scrollable
-      overflow-y: auto;
-    }
-  }
-}
-
-
-.page-comments {
-  // TODO: Never use .page-comments-list-toggle-older class.
-  .page-comments-list-toggle-older {
-    display: inline-block;
-    font-size: 0.9em;
-  }
-  // TODO: "pointer-events: none;" moved to "Comment.module.scss" now.
-  // .page-comment was defined in _comment.scss and _comment_growi.scss
-  // Required if .page-comment is not under .growi but under .page-comments, or under .growi but not under .page-comments
-  .page-comment {
-    padding-top: 50px;
-    margin-top: -50px;
-    pointer-events: none;
-  }
-}

+ 0 - 20
packages/app/src/styles/_comment_growi.scss

@@ -1,20 +0,0 @@
-.growi {
-  // display cheatsheet for comment form only
-  .comment-form {
-    // TODO: Never use .editor-cheatsheet class.
-    .editor-cheatsheet {
-      display: none;
-    }
-
-    // textarea
-    // TODO: Never use .comment-form-comment class.
-    .comment-form-comment {
-      height: 80px;
-      &:focus,
-      &:not(:invalid) {
-        height: 180px;
-        transition: height 0.2s ease-out;
-      }
-    }
-  }
-}

+ 0 - 6
packages/app/src/styles/_me.scss

@@ -1,6 +0,0 @@
-.user-settings-page {
-  .title {
-    @include variable-font-size(28px);
-    line-height: 1.1em;
-  }
-}

+ 0 - 9
packages/app/src/styles/_old-ios.scss

@@ -1,9 +0,0 @@
-html[old-ios] .layout-root:not(.editing) {
-  .grw-navbar {
-    position: initial !important;
-    top: 0 !important;
-  }
-  .grw-subnav-fixed-container {
-    top: 0 !important;
-  }
-}

+ 0 - 9
packages/app/src/styles/_page-duplicate-modal.scss

@@ -1,9 +0,0 @@
-.grw-duplicate-page {
-  .duplicate-name {
-    list-style: none;
-  }
-
-  .duplicate-exist {
-    color: #c7254e;
-  }
-}

+ 0 - 20
packages/app/src/styles/_user.scss

@@ -1,20 +0,0 @@
-$easeInOutCubic: cubic-bezier(0.65, 0, 0.35, 1);
-
-%transitionForCompactMode {
-  // set transition-duration (normal -> compact)
-  transition: all 300ms $easeInOutCubic;
-}
-
-/*
- * Styles
- */
-
-.draft-list-item {
-  .icon-container {
-    .icon-copy,
-    .draft-delete,
-    .icon-edit {
-      cursor: pointer;
-    }
-  }
-}

+ 2 - 0
packages/app/src/styles/_variables.scss

@@ -38,3 +38,5 @@ $grw-logomark-width: 36px;
 $grw-nav-main-left-tab-width: 95px;
 $grw-nav-main-left-tab-width-mobile: 50px;
 $grw-nav-main-tab-height: 42px;
+
+$grw-scroll-margin-top-in-view: 130px;

+ 2 - 0
packages/app/src/styles/organisms/_wiki.scss

@@ -30,6 +30,8 @@
     &:first-child {
       margin-top: 0;
     }
+
+    scroll-margin-top: var.$grw-scroll-margin-top-in-view;
   }
 
   h1 {

+ 0 - 7
packages/app/src/styles/style-app.scss

@@ -40,23 +40,16 @@
 @import 'organisms/wiki';
 
 // // growi component
-// @import 'attachments';
-// @import 'comment';
-// @import 'comment_growi';
 // @import 'draft';
 @import 'editor';
 @import 'fonts';
 @import 'layout';
-// @import 'me';
 @import 'mirror_mode';
 @import 'modal';
-// @import 'old-ios';
-// @import 'page-duplicate-modal';
 @import 'page-path';
 @import 'search';
 @import 'tag';
 @import 'installer';
-// @import 'user';
 
 
 /*

+ 13 - 23
yarn.lock

@@ -4282,11 +4282,6 @@
   dependencies:
     "@types/unist" "*"
 
-"@types/mdurl@^1.0.0":
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-1.0.2.tgz#e2ce9d83a613bacf284c7be7d491945e39e1f8e9"
-  integrity sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==
-
 "@types/mime-types@^2.1.0":
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/@types/mime-types/-/mime-types-2.1.1.tgz#d9ba43490fa3a3df958759adf69396c3532cf2c1"
@@ -14440,18 +14435,15 @@ mdast-util-mdx-expression@^1.1.0:
     mdast-util-to-markdown "^1.0.0"
 
 mdast-util-to-hast@^12.1.0:
-  version "12.1.2"
-  resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-12.1.2.tgz#5c793b04014746585254c7ce0bc2d117201a5d1d"
-  integrity sha512-Wn6Mcj04qU4qUXHnHpPATYMH2Jd8RlntdnloDfYLe1ErWRHo6+pvSl/DzHp6sCZ9cBSYlc8Sk8pbwb8xtUoQhQ==
+  version "12.3.0"
+  resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-12.3.0.tgz#045d2825fb04374e59970f5b3f279b5700f6fb49"
+  integrity sha512-pits93r8PhnIoU4Vy9bjW39M2jJ6/tdHyja9rrot9uujkN7UTU9SDnE6WNJz/IGyQk3XHX6yNNtrBH6cQzm8Hw==
   dependencies:
     "@types/hast" "^2.0.0"
     "@types/mdast" "^3.0.0"
-    "@types/mdurl" "^1.0.0"
     mdast-util-definitions "^5.0.0"
-    mdurl "^1.0.0"
-    micromark-util-sanitize-uri "^1.0.0"
+    micromark-util-sanitize-uri "^1.1.0"
     trim-lines "^3.0.0"
-    unist-builder "^3.0.0"
     unist-util-generated "^2.0.0"
     unist-util-position "^4.0.0"
     unist-util-visit "^4.0.0"
@@ -14526,10 +14518,6 @@ mdast-util-wiki-link@^0.0.2:
     "@babel/runtime" "^7.12.1"
     mdast-util-to-markdown "^0.6.5"
 
-mdurl@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e"
-
 media-typer@0.3.0:
   version "0.3.0"
   resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
@@ -15013,6 +15001,15 @@ micromark-util-sanitize-uri@^1.0.0:
     micromark-util-encode "^1.0.0"
     micromark-util-symbol "^1.0.0"
 
+micromark-util-sanitize-uri@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.1.0.tgz#f12e07a85106b902645e0364feb07cf253a85aee"
+  integrity sha512-RoxtuSCX6sUNtxhbmsEFQfWzs8VN7cTctmBPvYivo98xb/kDEoTCtJQX5wyzIYEmk/lvNFTat4hL8oW0KndFpg==
+  dependencies:
+    micromark-util-character "^1.0.0"
+    micromark-util-encode "^1.0.0"
+    micromark-util-symbol "^1.0.0"
+
 micromark-util-subtokenize@^1.0.0:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/micromark-util-subtokenize/-/micromark-util-subtokenize-1.0.2.tgz#ff6f1af6ac836f8bfdbf9b02f40431760ad89105"
@@ -23069,13 +23066,6 @@ unique-string@^2.0.0:
   dependencies:
     crypto-random-string "^2.0.0"
 
-unist-builder@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/unist-builder/-/unist-builder-3.0.0.tgz#728baca4767c0e784e1e64bb44b5a5a753021a04"
-  integrity sha512-GFxmfEAa0vi9i5sd0R2kcrI9ks0r82NasRq5QHh2ysGngrc6GiqD5CDf1FjPenY4vApmFASBIIlk/jj5J5YbmQ==
-  dependencies:
-    "@types/unist" "^2.0.0"
-
 unist-types@^1.1.5:
   version "1.1.5"
   resolved "https://registry.yarnpkg.com/unist-types/-/unist-types-1.1.5.tgz#f001c0af2c3b0ac6e4f5d9aa114e7afe046d7abe"