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

Merge branch 'master' into fix/114374-114406-disable-to-show-password-reset-info

jam411 3 лет назад
Родитель
Сommit
9a2f6ed62e
65 измененных файлов с 426 добавлено и 497 удалено
  1. 5 2
      .github/ISSUE_TEMPLATE/config.yml
  2. 0 25
      .github/ISSUE_TEMPLATE/user-request.md
  3. 2 0
      .github/workflows/ci-app-prod.yml
  4. 1 0
      .github/workflows/ci-app.yml
  5. 10 0
      bin/data-migrations/v6/src/processor.js
  6. 1 2
      packages/app/package.json
  7. 1 0
      packages/app/public/static/locales/en_US/admin.json
  8. 1 0
      packages/app/public/static/locales/ja_JP/admin.json
  9. 1 0
      packages/app/public/static/locales/zh_CN/admin.json
  10. 0 1
      packages/app/src/client/services/AdminAppContainer.js
  11. 0 5
      packages/app/src/client/util/smooth-scroll.ts
  12. 1 1
      packages/app/src/components/Admin/App/AwsSetting.tsx
  13. 52 23
      packages/app/src/components/Comments.tsx
  14. 2 5
      packages/app/src/components/ContentLinkButtons.tsx
  15. 1 2
      packages/app/src/components/Fab.tsx
  16. 30 3
      packages/app/src/components/Page/PageView.tsx
  17. 4 22
      packages/app/src/components/Page/RevisionLoader.tsx
  18. 3 2
      packages/app/src/components/PageComment/Comment.module.scss
  19. 8 0
      packages/app/src/components/PageDeleteModal.tsx
  20. 14 4
      packages/app/src/components/PageEditor.tsx
  21. 1 2
      packages/app/src/components/PageSideContents.tsx
  22. 1 2
      packages/app/src/components/PageTimeline.tsx
  23. 14 1
      packages/app/src/components/ReactMarkdownComponents/Header.tsx
  24. 1 8
      packages/app/src/components/ReactMarkdownComponents/NextLink.tsx
  25. 10 10
      packages/app/src/components/SearchPage/SearchResultContent.tsx
  26. 6 4
      packages/app/src/components/Sidebar/InfiniteScroll.tsx
  27. 5 5
      packages/app/src/components/Sidebar/PageTree/ItemsTree.tsx
  28. 12 11
      packages/app/src/components/Sidebar/RecentChanges.tsx
  29. 4 7
      packages/app/src/pages/[[...path]].page.tsx
  30. 3 7
      packages/app/src/pages/_private-legacy-pages.page.tsx
  31. 3 7
      packages/app/src/pages/_search.page.tsx
  32. 3 7
      packages/app/src/pages/me/[[...path]].page.tsx
  33. 3 6
      packages/app/src/pages/tags.page.tsx
  34. 3 7
      packages/app/src/pages/trash.page.tsx
  35. 17 3
      packages/app/src/pages/utils/commons.ts
  36. 8 4
      packages/app/src/server/routes/apiv3/app-settings.js
  37. 2 3
      packages/app/src/server/routes/apiv3/pages.js
  38. 15 5
      packages/app/src/server/routes/apiv3/users.js
  39. 4 1
      packages/app/src/services/renderer/rehype-plugins/relative-links.ts
  40. 17 6
      packages/app/src/services/renderer/renderer.tsx
  41. 18 12
      packages/app/src/stores/page-listing.tsx
  42. 3 13
      packages/app/src/stores/renderer.tsx
  43. 4 14
      packages/app/src/stores/ui.tsx
  44. 0 3
      packages/app/src/styles/_attachments.scss
  45. 0 27
      packages/app/src/styles/_comment.scss
  46. 0 20
      packages/app/src/styles/_comment_growi.scss
  47. 0 6
      packages/app/src/styles/_me.scss
  48. 0 9
      packages/app/src/styles/_old-ios.scss
  49. 0 9
      packages/app/src/styles/_page-duplicate-modal.scss
  50. 0 20
      packages/app/src/styles/_user.scss
  51. 2 0
      packages/app/src/styles/_variables.scss
  52. 2 0
      packages/app/src/styles/organisms/_wiki.scss
  53. 0 7
      packages/app/src/styles/style-app.scss
  54. 4 2
      packages/app/src/styles/theme/_apply-colors-dark.scss
  55. 4 2
      packages/app/src/styles/theme/_apply-colors-light.scss
  56. 2 5
      packages/app/src/styles/theme/_apply-colors.scss
  57. 25 28
      packages/preset-themes/src/styles/antarctic.scss
  58. 10 11
      packages/preset-themes/src/styles/christmas.scss
  59. 29 29
      packages/preset-themes/src/styles/hufflepuff.scss
  60. 15 13
      packages/preset-themes/src/styles/spring.scss
  61. 10 8
      packages/preset-themes/src/styles/wood.scss
  62. 1 1
      packages/remark-lsx/package.json
  63. 2 2
      packages/remark-lsx/src/components/Lsx.tsx
  64. 9 7
      packages/remark-lsx/src/stores/lsx.tsx
  65. 17 46
      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

+ 2 - 0
.github/workflows/ci-app-prod.yml

@@ -14,6 +14,7 @@ on:
       - '!packages/app/docker/**'
       - packages/codemirror-textlint/**
       - packages/core/**
+      - packages/preset-themes/**
       - packages/remark-*/**
       - packages/slack/**
       - packages/ui/**
@@ -31,6 +32,7 @@ on:
       - '!packages/app/docker/**'
       - packages/codemirror-textlint/**
       - packages/core/**
+      - packages/preset-themes/**
       - packages/remark-*/**
       - packages/slack/**
       - packages/ui/**

+ 1 - 0
.github/workflows/ci-app.yml

@@ -14,6 +14,7 @@ on:
       - '!packages/app/docker/**'
       - packages/codemirror-textlint/**
       - packages/core/**
+      - packages/preset-themes/**
       - packages/remark-*/**
       - packages/slack/**
       - packages/ui/**

+ 10 - 0
bin/data-migrations/v6/src/processor.js

@@ -31,6 +31,13 @@ function bracketlinkProcessor(body) {
   return body.replace(oldBracketLinkRegExp, '[[$1]]');
 }
 
+// processor for MIGRATION_TYPE=custom
+function customProcessor(body) {
+  // ADD YOUR PROCESS HERE!
+  // https://github.com/weseek/growi/discussions/7180
+  return body;
+}
+
 // ===========================================
 // define processors
 // ===========================================
@@ -56,6 +63,9 @@ function getProcessorArray(migrationType) {
     case 'v6':
       oldFormatProcessors = [drawioProcessor, plantumlProcessor, tsvProcessor, csvProcessor, bracketlinkProcessor];
       break;
+    case 'custom':
+      oldFormatProcessors = [customProcessor];
+      break;
     default:
       oldFormatProcessors = [];
   }

+ 1 - 2
packages/app/package.json

@@ -185,7 +185,7 @@
     "string-width": "=4.2.2",
     "superjson": "^1.9.1",
     "swagger-jsdoc": "^6.1.0",
-    "swr": "^2.0.2",
+    "swr": "^2.0.3",
     "throttle-debounce": "^3.0.1",
     "toastr": "^2.1.2",
     "uglifycss": "^0.0.29",
@@ -237,7 +237,6 @@
     "react-copy-to-clipboard": "^5.0.1",
     "react-dropzone": "^11.2.4",
     "react-hotkeys": "^2.0.0",
-    "react-waypoint": "^10.1.0",
     "rehype-rewrite": "^3.0.6",
     "replacestream": "^4.0.3",
     "reveal.js": "^4.3.1",

+ 1 - 0
packages/app/public/static/locales/en_US/admin.json

@@ -383,6 +383,7 @@
     "bucket_name": "Bucket name",
     "custom_endpoint": "Custom endpoint",
     "custom_endpoint_change": "Input the URL of the endpoint of an object storage service like MinIO that has a S3-compatible API.  Amazon S3 is used if empty.",
+    "s3_secret_access_key_input_description": "Setting value is hidden",
     "load_plugins": "Load plugins",
     "enable": "Enable",
     "disable": "Disable",

+ 1 - 0
packages/app/public/static/locales/ja_JP/admin.json

@@ -391,6 +391,7 @@
     "bucket_name": "バケット名",
     "custom_endpoint": "カスタムエンドポイント",
     "custom_endpoint_change": "MinIOなど、S3互換APIを持つ他のオブジェクトストレージサービスを使用する場合のみ、そのエンドポイントのURLを入力してください。空欄の場合は、Amazon S3を使用します。",
+    "s3_secret_access_key_input_description": "設定値は非表示です",
     "load_plugins": "プラグインを読み込む",
     "enable": "有効",
     "disable": "無効",

+ 1 - 0
packages/app/public/static/locales/zh_CN/admin.json

@@ -391,6 +391,7 @@
     "bucket_name": "Bucket name",
     "custom_endpoint": "Custom endpoint",
     "custom_endpoint_change": "输入对象存储服务(如MinIO)端点的URL,MinIO具有与S3兼容的API。如果为空,则使用Amazon S3。",
+    "s3_secret_access_key_input_description": "设定的值被隐藏。",
     "load_plugins": "加载插件",
     "enable": "启用",
     "disable": "停用",

+ 0 - 1
packages/app/src/client/services/AdminAppContainer.js

@@ -108,7 +108,6 @@ export default class AdminAppContainer extends Container {
       s3CustomEndpoint: appSettingsParams.s3CustomEndpoint,
       s3Bucket: appSettingsParams.s3Bucket,
       s3AccessKeyId: appSettingsParams.s3AccessKeyId,
-      s3SecretAccessKey: appSettingsParams.s3SecretAccessKey,
       s3ReferenceFileWithRelayMode: appSettingsParams.s3ReferenceFileWithRelayMode,
 
       gcsUseOnlyEnvVars: appSettingsParams.gcsUseOnlyEnvVars,

+ 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,
-};

+ 1 - 1
packages/app/src/components/Admin/App/AwsSetting.tsx

@@ -140,11 +140,11 @@ export const AwsSettingMolecule = (props: AwsSettingMoleculeProps): JSX.Element
           <input
             className="form-control"
             type="text"
-            defaultValue={props.s3SecretAccessKey || ''}
             onChange={(e) => {
               props?.onChangeS3SecretAccessKey(e.target.value);
             }}
           />
+          <p className="form-text text-muted">{t('admin:app_setting.s3_secret_access_key_input_description')}</p>
         </div>
       </div>
 

+ 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 (

+ 30 - 3
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;
@@ -61,7 +70,7 @@ export const PageView = (props: Props): JSX.Element => {
   const { data: viewOptions, mutate: mutateRendererOptions } = useViewOptions();
 
   const page = pageBySWR ?? initialPage;
-  const isNotFound = isNotFoundMeta || page == null;
+  const isNotFound = isNotFoundMeta || page?.revision == null;
   const isUsersHomePagePath = isUsersHomePage(pagePath);
 
   // register to facade
@@ -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}/>
         ) }

+ 4 - 22
packages/app/src/components/Page/RevisionLoader.tsx

@@ -1,8 +1,7 @@
-import React, { useEffect, useState, useCallback } from 'react';
+import React, { useState, useCallback, useEffect } from 'react';
 
 import { Ref, IRevision, IRevisionHasId } from '@growi/core';
 import { useTranslation } from 'next-i18next';
-import { Waypoint } from 'react-waypoint';
 
 import { apiv3Get } from '~/client/util/apiv3-client';
 import { RendererOptions } from '~/services/renderer/renderer';
@@ -16,7 +15,6 @@ export type RevisionLoaderProps = {
   rendererOptions: RendererOptions,
   pageId: string,
   revisionId: Ref<IRevision>,
-  lazy?: boolean,
   onRevisionLoaded?: (revision: IRevisionHasId) => void,
 }
 
@@ -34,7 +32,7 @@ export const RevisionLoader = (props: RevisionLoaderProps): JSX.Element => {
   const { t } = useTranslation();
 
   const {
-    rendererOptions, pageId, revisionId, lazy, onRevisionLoaded,
+    rendererOptions, pageId, revisionId, onRevisionLoaded,
   } = props;
 
   const [isLoading, setIsLoading] = useState<boolean>();
@@ -69,24 +67,8 @@ export const RevisionLoader = (props: RevisionLoaderProps): JSX.Element => {
   }, [isLoaded, isLoading, onRevisionLoaded, pageId, revisionId]);
 
   useEffect(() => {
-    if (!lazy) {
-      loadData();
-    }
-  }, [lazy, loadData]);
-
-  const onWaypointChange = (event) => {
-    if (event.currentPosition === Waypoint.above || event.currentPosition === Waypoint.inside) {
-      loadData();
-    }
-    return;
-  };
-
-  /* ----- before load ----- */
-  if (lazy && !isLoaded) {
-    return (
-      <Waypoint onPositionChange={onWaypointChange} bottomOffset="-100px" />
-    );
-  }
+    loadData();
+  }, [loadData]);
 
   /* ----- loading ----- */
   if (isLoading) {

+ 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"

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

@@ -32,7 +32,6 @@ const TimelineCard = ({ page }: TimelineCardProps): JSX.Element => {
       <div className="card-body">
         { rendererOptions != null && (
           <RevisionLoader
-            lazy
             rendererOptions={rendererOptions}
             pageId={page._id}
             revisionId={page.revision}
@@ -55,7 +54,7 @@ export const PageTimeline = (): JSX.Element => {
 
   const handlePage = useCallback(async(selectedPage: number) => {
     if (currentPagePath == null) { return }
-    const res = await apiv3Get('/pages/list', { path: currentPagePath, selectedPage });
+    const res = await apiv3Get('/pages/list', { path: currentPagePath, page: selectedPage });
     setTotalPageItems(res.data.totalCount);
     setPages(res.data.pages);
     setLimit(res.data.limit);

+ 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;
 

+ 6 - 4
packages/app/src/components/Sidebar/InfiniteScroll.tsx

@@ -1,11 +1,12 @@
 import React, {
   Ref, useEffect, useState,
 } from 'react';
+
 import type { SWRInfiniteResponse } from 'swr/infinite';
 
 type Props<T> = {
   swrInifiniteResponse : SWRInfiniteResponse<T>
-  children: React.ReactChild | ((item: T) => React.ReactNode),
+  children: React.ReactNode,
   loadingIndicator?: React.ReactNode
   endingIndicator?: React.ReactNode
   isReachingEnd?: boolean,
@@ -39,7 +40,7 @@ const LoadingIndicator = (): React.ReactElement => {
 const InfiniteScroll = <E, >(props: Props<E>): React.ReactElement<Props<E>> => {
   const {
     swrInifiniteResponse: {
-      setSize, data, isValidating,
+      setSize, isValidating,
     },
     children,
     loadingIndicator,
@@ -54,11 +55,12 @@ const InfiniteScroll = <E, >(props: Props<E>): React.ReactElement<Props<E>> => {
     if (intersecting && !isValidating && !isReachingEnd) {
       setSize(size => size + 1);
     }
-  }, [setSize, intersecting]);
+  }, [setSize, intersecting, isValidating, isReachingEnd]);
 
   return (
     <>
-      {typeof children === 'function' ? data?.map(item => children(item)) : children}
+      { children }
+
       <div style={{ position: 'relative' }}>
         <div ref={ref} style={{ position: 'absolute', top: offset }}></div>
         {isReachingEnd

+ 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}`);
       }
     };
 

+ 12 - 11
packages/app/src/components/Sidebar/RecentChanges.tsx

@@ -10,7 +10,7 @@ import Link from 'next/link';
 import PagePathHierarchicalLink from '~/components/PagePathHierarchicalLink';
 import { IPageHasId } from '~/interfaces/page';
 import LinkedPagePath from '~/models/linked-page-path';
-import { useSWRInifinitexRecentlyUpdated } from '~/stores/page-listing';
+import { useSWRINFxRecentlyUpdated } from '~/stores/page-listing';
 import loggerFactory from '~/utils/logger';
 
 import FormattedDistanceDate from '../FormattedDistanceDate';
@@ -19,7 +19,6 @@ import InfiniteScroll from './InfiniteScroll';
 import { SidebarHeaderReloadButton } from './SidebarHeaderReloadButton';
 import RecentChangesContentSkeleton from './Skeleton/RecentChangesContentSkeleton';
 
-import TagLabelsStyles from '../Page/TagLabels.module.scss';
 import styles from './RecentChanges.module.scss';
 
 
@@ -104,13 +103,14 @@ const RecentChanges = (): JSX.Element => {
 
   const PER_PAGE = 20;
   const { t } = useTranslation();
-  const swrInifinitexRecentlyUpdated = useSWRInifinitexRecentlyUpdated();
-  const { data: dataRecentlyUpdated, error, mutate: mutateRecentlyUpdated } = swrInifinitexRecentlyUpdated;
+  const swrInifinitexRecentlyUpdated = useSWRINFxRecentlyUpdated(PER_PAGE);
+  const {
+    data, mutate, isLoading,
+  } = swrInifinitexRecentlyUpdated;
 
   const [isRecentChangesSidebarSmall, setIsRecentChangesSidebarSmall] = useState(false);
-  const isEmpty = dataRecentlyUpdated?.[0].length === 0;
-  const isLoading = error == null && dataRecentlyUpdated === undefined;
-  const isReachingEnd = isEmpty || (dataRecentlyUpdated && dataRecentlyUpdated[dataRecentlyUpdated.length - 1]?.length < PER_PAGE);
+  const isEmpty = data?.[0]?.pages.length === 0;
+  const isReachingEnd = isEmpty || (data != null && data[data.length - 1]?.pages.length < PER_PAGE);
 
   const retrieveSizePreferenceFromLocalStorage = useCallback(() => {
     if (window.localStorage.isRecentChangesSidebarSmall === 'true') {
@@ -132,7 +132,7 @@ const RecentChanges = (): JSX.Element => {
     <div className="px-3" data-testid="grw-recent-changes">
       <div className="grw-sidebar-content-header py-3 d-flex">
         <h3 className="mb-0 text-nowrap">{t('Recent Changes')}</h3>
-        <SidebarHeaderReloadButton onClick={() => mutateRecentlyUpdated()}/>
+        <SidebarHeaderReloadButton onClick={() => mutate()}/>
         <div className="d-flex align-items-center">
           <div className={`grw-recent-changes-resize-button ${styles['grw-recent-changes-resize-button']} custom-control custom-switch ml-1`}>
             <input
@@ -155,9 +155,10 @@ const RecentChanges = (): JSX.Element => {
                 swrInifiniteResponse={swrInifinitexRecentlyUpdated}
                 isReachingEnd={isReachingEnd}
               >
-                {pages => pages.map(
-                  page => <PageItem key={page._id} page={page} isSmall={isRecentChangesSidebarSmall} />,
-                )
+                { data != null && data.map(apiResult => apiResult.pages).flat()
+                  .map(page => (
+                    <PageItem key={page._id} page={page} isSmall={isRecentChangesSidebarSmall} />
+                  ))
                 }
               </InfiniteScroll>
             </ul>

+ 4 - 7
packages/app/src/pages/[[...path]].page.tsx

@@ -63,7 +63,7 @@ import {
 
 import { NextPageWithLayout } from './_app.page';
 import {
-  CommonProps, getNextI18NextConfig, getServerSideCommonProps, generateCustomTitleForPage,
+  CommonProps, getNextI18NextConfig, getServerSideCommonProps, generateCustomTitleForPage, useInitSidebarConfig,
 } from './utils/commons';
 
 
@@ -199,12 +199,8 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
   useEditorConfig(props.editorConfig);
   useCsrfToken(props.csrfToken);
 
-  // UserUISettings
-  usePreferDrawerModeByUser(props.userUISettings?.preferDrawerModeByUser ?? props.sidebarConfig.isSidebarDrawerMode);
-  usePreferDrawerModeOnEditByUser(props.userUISettings?.preferDrawerModeOnEditByUser);
-  useSidebarCollapsed(props.userUISettings?.isSidebarCollapsed ?? props.sidebarConfig.isSidebarClosedAtDockMode);
-  useCurrentSidebarContents(props.userUISettings?.currentSidebarContents);
-  useCurrentProductNavWidth(props.userUISettings?.currentProductNavWidth);
+  // init sidebar config with UserUISettings and sidebarConfig
+  useInitSidebarConfig(props.sidebarConfig, props.userUISettings);
 
   // page
   useIsLatestRevision(props.isLatestRevision);
@@ -500,6 +496,7 @@ async function injectRoutingInformation(context: GetServerSidePropsContext, prop
   else {
     props.isNotFound = page.isEmpty;
     props.isNotCreatable = false;
+    props.isForbidden = false;
     // /62a88db47fed8b2d94f30000 ==> /path/to/page
     if (isPermalink && page.isEmpty) {
       props.currentPathname = page.path;

+ 3 - 7
packages/app/src/pages/_private-legacy-pages.page.tsx

@@ -23,7 +23,7 @@ import {
 } from '~/stores/ui';
 
 import {
-  CommonProps, getNextI18NextConfig, getServerSideCommonProps, generateCustomTitle,
+  CommonProps, getNextI18NextConfig, getServerSideCommonProps, generateCustomTitle, useInitSidebarConfig,
 } from './utils/commons';
 
 const SearchResultLayout = dynamic(() => import('~/components/Layout/SearchResultLayout'), { ssr: false });
@@ -67,12 +67,8 @@ const PrivateLegacyPage: NextPage<Props> = (props: Props) => {
 
   useDrawioUri(props.drawioUri);
 
-  // UserUISettings
-  usePreferDrawerModeByUser(userUISettings?.preferDrawerModeByUser ?? props.sidebarConfig.isSidebarDrawerMode);
-  usePreferDrawerModeOnEditByUser(userUISettings?.preferDrawerModeOnEditByUser);
-  useSidebarCollapsed(userUISettings?.isSidebarCollapsed ?? props.sidebarConfig.isSidebarClosedAtDockMode);
-  useCurrentSidebarContents(userUISettings?.currentSidebarContents);
-  useCurrentProductNavWidth(userUISettings?.currentProductNavWidth);
+  // init sidebar config with UserUISettings and sidebarConfig
+  useInitSidebarConfig(props.sidebarConfig, props.userUISettings);
 
   // render config
   useRendererConfig(props.rendererConfig);

+ 3 - 7
packages/app/src/pages/_search.page.tsx

@@ -25,7 +25,7 @@ import { SearchPage } from '../components/SearchPage';
 
 import type { NextPageWithLayout } from './_app.page';
 import {
-  getNextI18NextConfig, getServerSideCommonProps, generateCustomTitle, CommonProps,
+  getNextI18NextConfig, getServerSideCommonProps, generateCustomTitle, CommonProps, useInitSidebarConfig,
 } from './utils/commons';
 
 
@@ -71,12 +71,8 @@ const SearchResultPage: NextPageWithLayout<Props> = (props: Props) => {
 
   useDrawioUri(props.drawioUri);
 
-  // UserUISettings
-  usePreferDrawerModeByUser(userUISettings?.preferDrawerModeByUser ?? props.sidebarConfig.isSidebarDrawerMode);
-  usePreferDrawerModeOnEditByUser(userUISettings?.preferDrawerModeOnEditByUser);
-  useSidebarCollapsed(userUISettings?.isSidebarCollapsed ?? props.sidebarConfig.isSidebarClosedAtDockMode);
-  useCurrentSidebarContents(userUISettings?.currentSidebarContents);
-  useCurrentProductNavWidth(userUISettings?.currentProductNavWidth);
+  // init sidebar config with UserUISettings and sidebarConfig
+  useInitSidebarConfig(props.sidebarConfig, props.userUISettings);
 
   // render config
   useRendererConfig(props.rendererConfig);

+ 3 - 7
packages/app/src/pages/me/[[...path]].page.tsx

@@ -30,7 +30,7 @@ import loggerFactory from '~/utils/logger';
 
 import { NextPageWithLayout } from '../_app.page';
 import {
-  CommonProps, getNextI18NextConfig, getServerSideCommonProps, generateCustomTitle,
+  CommonProps, getNextI18NextConfig, getServerSideCommonProps, generateCustomTitle, useInitSidebarConfig,
 } from '../utils/commons';
 
 
@@ -97,12 +97,8 @@ const MePage: NextPageWithLayout<Props> = (props: Props) => {
   // commons
   useCsrfToken(props.csrfToken);
 
-  // UserUISettings
-  usePreferDrawerModeByUser(props.userUISettings?.preferDrawerModeByUser ?? props.sidebarConfig.isSidebarDrawerMode);
-  usePreferDrawerModeOnEditByUser(props.userUISettings?.preferDrawerModeOnEditByUser);
-  useSidebarCollapsed(props.userUISettings?.isSidebarCollapsed ?? props.sidebarConfig.isSidebarClosedAtDockMode);
-  useCurrentSidebarContents(props.userUISettings?.currentSidebarContents);
-  useCurrentProductNavWidth(props.userUISettings?.currentProductNavWidth);
+  // init sidebar config with UserUISettings and sidebarConfig
+  useInitSidebarConfig(props.sidebarConfig, props.userUISettings);
 
   // page
   useIsSearchServiceConfigured(props.isSearchServiceConfigured);

+ 3 - 6
packages/app/src/pages/tags.page.tsx

@@ -27,7 +27,7 @@ import {
 
 import { NextPageWithLayout } from './_app.page';
 import {
-  CommonProps, getServerSideCommonProps, getNextI18NextConfig, generateCustomTitle,
+  CommonProps, getServerSideCommonProps, getNextI18NextConfig, generateCustomTitle, useInitSidebarConfig,
 } from './utils/commons';
 
 const PAGING_LIMIT = 10;
@@ -72,11 +72,8 @@ const TagPage: NextPageWithLayout<CommonProps> = (props: Props) => {
   useIsSearchServiceReachable(props.isSearchServiceReachable);
   useIsSearchScopeChildrenAsDefault(props.isSearchScopeChildrenAsDefault);
 
-  usePreferDrawerModeByUser(props.userUISettings?.preferDrawerModeByUser ?? props.sidebarConfig.isSidebarDrawerMode);
-  usePreferDrawerModeOnEditByUser(props.userUISettings?.preferDrawerModeOnEditByUser);
-  useSidebarCollapsed(props.userUISettings?.isSidebarCollapsed ?? props.sidebarConfig.isSidebarClosedAtDockMode);
-  useCurrentSidebarContents(props.userUISettings?.currentSidebarContents);
-  useCurrentProductNavWidth(props.userUISettings?.currentProductNavWidth);
+  // init sidebar config with UserUISettings and sidebarConfig
+  useInitSidebarConfig(props.sidebarConfig, props.userUISettings);
 
   useRendererConfig(props.rendererConfig);
 

+ 3 - 7
packages/app/src/pages/trash.page.tsx

@@ -25,7 +25,7 @@ import {
 
 import type { NextPageWithLayout } from './_app.page';
 import {
-  getServerSideCommonProps, getNextI18NextConfig, generateCustomTitleForPage, CommonProps,
+  getServerSideCommonProps, getNextI18NextConfig, generateCustomTitleForPage, CommonProps, useInitSidebarConfig,
 } from './utils/commons';
 
 const TrashPageList = dynamic(() => import('~/components/TrashPageList').then(mod => mod.TrashPageList), { ssr: false });
@@ -58,12 +58,8 @@ const TrashPage: NextPageWithLayout<CommonProps> = (props: Props) => {
   useCurrentPageId(null);
   useCurrentPathname('/trash');
 
-  // UserUISettings
-  usePreferDrawerModeByUser(props.userUISettings?.preferDrawerModeByUser ?? props.sidebarConfig.isSidebarDrawerMode);
-  usePreferDrawerModeOnEditByUser(props.userUISettings?.preferDrawerModeOnEditByUser);
-  useSidebarCollapsed(props.userUISettings?.isSidebarCollapsed ?? props.sidebarConfig.isSidebarClosedAtDockMode);
-  useCurrentSidebarContents(props.userUISettings?.currentSidebarContents);
-  useCurrentProductNavWidth(props.userUISettings?.currentProductNavWidth);
+  // init sidebar config with UserUISettings and sidebarConfig
+  useInitSidebarConfig(props.sidebarConfig, props.userUISettings);
 
   useShowPageLimitationXL(props.showPageLimitationXL);
 

+ 17 - 3
packages/app/src/pages/utils/commons.ts

@@ -1,13 +1,18 @@
-import type { ColorScheme, IUser, IUserHasId } from '@growi/core';
+import type { ColorScheme, IUserHasId } from '@growi/core';
 import {
   DevidedPagePath, Lang, AllLang,
 } from '@growi/core';
-import { GetServerSideProps, GetServerSidePropsContext } from 'next';
-import { SSRConfig, UserConfig } from 'next-i18next';
+import type { GetServerSideProps, GetServerSidePropsContext } from 'next';
+import type { SSRConfig, UserConfig } from 'next-i18next';
 
 import * as nextI18NextConfig from '^/config/next-i18next.config';
 
 import type { CrowiRequest } from '~/interfaces/crowi-request';
+import type { ISidebarConfig } from '~/interfaces/sidebar-config';
+import type { IUserUISettings } from '~/interfaces/user-ui-settings';
+import {
+  useCurrentProductNavWidth, useCurrentSidebarContents, usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser, useSidebarCollapsed,
+} from '~/stores/ui';
 
 export type CommonProps = {
   namespacesRequired: string[], // i18next
@@ -139,3 +144,12 @@ export const generateCustomTitleForPage = (props: CommonProps, pagePath: string)
     .replace('{{pagepath}}', pagePath)
     .replace('{{pagename}}', dPagePath.latter);
 };
+
+export const useInitSidebarConfig = (sidebarConfig: ISidebarConfig, userUISettings?: IUserUISettings): void => {
+  // UserUISettings
+  usePreferDrawerModeByUser(userUISettings?.preferDrawerModeByUser ?? sidebarConfig.isSidebarDrawerMode);
+  usePreferDrawerModeOnEditByUser(userUISettings?.preferDrawerModeOnEditByUser);
+  useSidebarCollapsed(userUISettings?.isSidebarCollapsed ?? sidebarConfig.isSidebarClosedAtDockMode);
+  useCurrentSidebarContents(userUISettings?.currentSidebarContents);
+  useCurrentProductNavWidth(userUISettings?.currentProductNavWidth);
+};

+ 8 - 4
packages/app/src/server/routes/apiv3/app-settings.js

@@ -1,4 +1,3 @@
-import { ErrorV3 } from '@growi/core';
 import { body } from 'express-validator';
 
 import { i18n } from '^/config/next-i18next.config';
@@ -9,6 +8,8 @@ import loggerFactory from '~/utils/logger';
 import { generateAddActivityMiddleware } from '../../middlewares/add-activity';
 import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
 
+import { ErrorV3 } from '@growi/core';
+
 
 const logger = loggerFactory('growi:routes:apiv3:app-settings');
 
@@ -252,7 +253,6 @@ module.exports = (crowi) => {
       s3CustomEndpoint: crowi.configManager.getConfig('crowi', 'aws:s3CustomEndpoint'),
       s3Bucket: crowi.configManager.getConfig('crowi', 'aws:s3Bucket'),
       s3AccessKeyId: crowi.configManager.getConfig('crowi', 'aws:s3AccessKeyId'),
-      s3SecretAccessKey: crowi.configManager.getConfig('crowi', 'aws:s3SecretAccessKey'),
       s3ReferenceFileWithRelayMode: crowi.configManager.getConfig('crowi', 'aws:referenceFileWithRelayMode'),
 
       gcsUseOnlyEnvVars: crowi.configManager.getConfig('crowi', 'gcs:useOnlyEnvVarsForSomeOptions'),
@@ -630,12 +630,17 @@ module.exports = (crowi) => {
       requestParams['aws:s3CustomEndpoint'] = req.body.s3CustomEndpoint;
       requestParams['aws:s3Bucket'] = req.body.s3Bucket;
       requestParams['aws:s3AccessKeyId'] = req.body.s3AccessKeyId;
-      requestParams['aws:s3SecretAccessKey'] = req.body.s3SecretAccessKey;
       requestParams['aws:referenceFileWithRelayMode'] = req.body.s3ReferenceFileWithRelayMode;
     }
 
     try {
       await crowi.configManager.updateConfigsInTheSameNamespace('crowi', requestParams, true);
+
+      const s3SecretAccessKey = req.body.s3SecretAccessKey;
+      if (fileUploadType === 'aws' && s3SecretAccessKey != null && s3SecretAccessKey.trim() !== '') {
+        await crowi.configManager.updateConfigsInTheSameNamespace('crowi', { 'aws:s3SecretAccessKey': s3SecretAccessKey }, true);
+      }
+
       await crowi.setUpFileUpload(true);
       crowi.fileUploaderSwitchService.publishUpdatedMessage();
 
@@ -655,7 +660,6 @@ module.exports = (crowi) => {
         responseParams.s3CustomEndpoint = crowi.configManager.getConfig('crowi', 'aws:s3CustomEndpoint');
         responseParams.s3Bucket = crowi.configManager.getConfig('crowi', 'aws:s3Bucket');
         responseParams.s3AccessKeyId = crowi.configManager.getConfig('crowi', 'aws:s3AccessKeyId');
-        responseParams.s3SecretAccessKey = crowi.configManager.getConfig('crowi', 'aws:s3SecretAccessKey');
         responseParams.s3ReferenceFileWithRelayMode = crowi.configManager.getConfig('crowi', 'aws:referenceFileWithRelayMode');
       }
       const parameters = { action: SupportedAction.ACTION_ADMIN_FILE_UPLOAD_CONFIG_UPDATE };

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

@@ -379,11 +379,10 @@ module.exports = (crowi) => {
    *
    */
   router.get('/recent', accessTokenParser, loginRequired, async(req, res) => {
-    const limit = 20;
+    const limit = parseInt(req.query.limit) || 20;
     const offset = parseInt(req.query.offset) || 0;
-    const skip = offset > 0 ? (offset - 1) * limit : offset;
     const queryOptions = {
-      offset: skip,
+      offset,
       limit,
       includeTrashed: false,
       isRegExpEscapedFromPath: true,

+ 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;
       }
 

+ 17 - 6
packages/app/src/services/renderer/renderer.tsx

@@ -12,7 +12,7 @@ import type { NormalComponents } from 'react-markdown/lib/complex-types';
 import type { ReactMarkdownOptions } from 'react-markdown/lib/react-markdown';
 import katex from 'rehype-katex';
 import raw from 'rehype-raw';
-import sanitize, { defaultSchema as sanitizeDefaultSchema } from 'rehype-sanitize';
+import sanitize, { defaultSchema as rehypeSanitizeDefaultSchema } from 'rehype-sanitize';
 import slug from 'rehype-slug';
 import type { HtmlElementNode } from 'rehype-toc';
 import breaks from 'remark-breaks';
@@ -67,13 +67,19 @@ export type RendererOptions = Omit<ReactMarkdownOptions, 'remarkPlugins' | 'rehy
     | undefined
 };
 
-const commonSanitizeAttributes = { '*': ['class', 'className', 'style'] };
+const baseSanitizeSchema = {
+  tagNames: ['iframe'],
+  attributes: {
+    iframe: ['allow', 'referrerpolicy', 'sandbox', 'src', 'srcdoc'],
+    '*': ['class', 'className', 'style'],
+  },
+};
 
 const commonSanitizeOption: SanitizeOption = deepmerge(
-  sanitizeDefaultSchema,
+  rehypeSanitizeDefaultSchema,
+  baseSanitizeSchema,
   {
     clobberPrefix: 'mdcont-',
-    attributes: commonSanitizeAttributes,
   },
 );
 
@@ -81,8 +87,8 @@ let isInjectedCustomSanitaizeOption = false;
 
 const injectCustomSanitizeOption = (config: RendererConfig) => {
   if (!isInjectedCustomSanitaizeOption && config.isEnabledXssPrevention && config.xssOption === RehypeSanitizeOption.CUSTOM) {
-    commonSanitizeOption.tagNames = config.tagWhiteList;
-    commonSanitizeOption.attributes = deepmerge(commonSanitizeAttributes, config.attrWhiteList ?? {});
+    commonSanitizeOption.tagNames = deepmerge(baseSanitizeSchema.tagNames, config.tagWhiteList ?? []);
+    commonSanitizeOption.attributes = deepmerge(baseSanitizeSchema.attributes, config.attrWhiteList ?? {});
     isInjectedCustomSanitaizeOption = true;
   }
 };
@@ -122,6 +128,10 @@ const generateCommonOptions = (pagePath: string|undefined): RendererOptions => {
       pukiwikiLikeLinker,
       growiDirective,
     ],
+    remarkRehypeOptions: {
+      clobberPrefix: 'mdcont-',
+      allowDangerousHtml: true,
+    },
     rehypePlugins: [
       [relativeLinksByPukiwikiLikeLinker, { pagePath }],
       [relativeLinks, { pagePath }],
@@ -324,6 +334,7 @@ export const generateSSRViewOptions = (
 
   // add rehype plugins
   rehypePlugins.push(
+    slug,
     [lsxGrowiPlugin.rehypePlugin, { pagePath, isSharedPage: config.isSharedPage }],
     rehypeSanitizePlugin,
     katex,

+ 18 - 12
packages/app/src/stores/page-listing.tsx

@@ -24,19 +24,25 @@ export const useSWRxPagesByPath = (path?: Nullable<string>): SWRResponse<IPageHa
   );
 };
 
-export const useSWRxRecentlyUpdated = (): SWRResponse<(IPageHasId)[], Error> => {
-  return useSWR(
-    '/pages/recent',
-    endpoint => apiv3Get<{ pages:(IPageHasId)[] }>(endpoint).then(response => response.data?.pages),
-  );
-};
-export const useSWRInifinitexRecentlyUpdated = () : SWRInfiniteResponse<(IPageHasId)[], Error> => {
-  const getKey = (page: number): string => {
-    return `/pages/recent?offset=${page + 1}`;
-  };
+
+type RecentApiResult = {
+  pages: IPageHasId[],
+  totalCount: number,
+  offset: number,
+}
+export const useSWRINFxRecentlyUpdated = (limit: number) : SWRInfiniteResponse<RecentApiResult, Error> => {
   return useSWRInfinite(
-    getKey,
-    endpoint => apiv3Get<{ pages:(IPageHasId)[] }>(endpoint).then(response => response.data?.pages),
+    (pageIndex, previousPageData) => {
+      if (previousPageData != null && previousPageData.pages.length === 0) return null;
+
+      if (pageIndex === 0 || previousPageData == null) {
+        return ['/pages/recent', undefined, limit];
+      }
+
+      const offset = previousPageData.offset + limit;
+      return ['/pages/recent', offset, limit];
+    },
+    ([endpoint, offset, limit]) => apiv3Get<RecentApiResult>(endpoint, { offset, limit }).then(response => response.data),
     {
       revalidateFirstPage: false,
       revalidateAll: false,

+ 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;
 

+ 4 - 14
packages/app/src/stores/ui.tsx

@@ -79,16 +79,11 @@ export const useIsMobile = (): SWRResponse<boolean, Error> => {
   return useStaticSWR<boolean, Error>(key, undefined, configuration);
 };
 
-// TODO: Enable `editing-sidebar` class
-// https://redmine.weseek.co.jp/issues/111527
-const getClassNamesByEditorMode = (editorMode: EditorMode | undefined /* , isSidebar = false */): string[] => {
+const getClassNamesByEditorMode = (editorMode: EditorMode | undefined): string[] => {
   const classNames: string[] = [];
   switch (editorMode) {
     case EditorMode.Editor:
       classNames.push('editing', 'builtin-editor');
-      // if (isSidebar) {
-      //   classNames.push('editing-sidebar');
-      // }
       break;
     case EditorMode.HackMD:
       classNames.push('editing', 'hackmd');
@@ -140,10 +135,8 @@ export const determineEditorModeByHash = (): EditorMode => {
   }
 };
 
-// TODO: Enable `editing-sidebar` class somehow
-// https://redmine.weseek.co.jp/issues/111527
 type EditorModeUtils = {
-  getClassNamesByEditorMode: (/* isEditingSidebar: boolean */) => string[],
+  getClassNamesByEditorMode: () => string[],
 }
 
 export const useEditorMode = (): SWRResponseWithUtils<EditorModeUtils, EditorMode> => {
@@ -171,11 +164,8 @@ export const useEditorMode = (): SWRResponseWithUtils<EditorModeUtils, EditorMod
     return mutateOriginal(editorMode, shouldRevalidate);
   }, [isEditable, mutateOriginal]);
 
-  // TODO: Enable `editing-sidebar` class
-  // https://redmine.weseek.co.jp/issues/111527
-  // construct getClassNamesByEditorMode method
-  const getClassNames = useCallback((/* isEditingSidebar: boolean */) => {
-    return getClassNamesByEditorMode(swrResponse.data /* , isEditingSidebar */);
+  const getClassNames = useCallback(() => {
+    return getClassNamesByEditorMode(swrResponse.data);
   }, [swrResponse.data]);
 
   return Object.assign(swrResponse, {

+ 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';
 
 
 /*

+ 4 - 2
packages/app/src/styles/theme/_apply-colors-dark.scss

@@ -519,8 +519,10 @@
     }
   }
 
-  .page-comments-row {
-    background: var(--bgcolor-subnav);
+  .page-comment-form .comment-form-main {
+    &:before {
+      border-right-color: var(--bgcolor-global);
+    }
   }
 
   /*

+ 4 - 2
packages/app/src/styles/theme/_apply-colors-light.scss

@@ -400,8 +400,10 @@
     }
   }
 
-  .page-comments-row {
-    background: var(--bgcolor-subnav);
+  .page-comment-form .comment-form-main {
+    &:before {
+      border-right-color: var(--bgcolor-global);
+    }
   }
 
   /*

+ 2 - 5
packages/app/src/styles/theme/_apply-colors.scss

@@ -567,15 +567,12 @@ ul.pagination {
 /*
  * GROWI comment form
  */
-.page-comments {
+.page-comments-row {
+  background: var(--bgcolor-subnav);
   .page-comment .page-comment-main,
   .page-comment-form .comment-form-main {
     background-color: var(--bgcolor-global);
 
-    &:before {
-      border-right-color: var(--bgcolor-global);
-    }
-
     .nav.nav-tabs {
       > li > a.active {
         background: transparent;

+ 25 - 28
packages/preset-themes/src/styles/antarctic.scss

@@ -3,28 +3,6 @@
 @use './theme/mixins/page-editor-mode-manager';
 @use './theme/hsl-functions' as hsl;
 
-.growi:not(.login-page) {
-  // add background-image
-  .page-editor-preview-container {
-    background-image: url('/images/themes/antarctic/bg.svg');
-    background-attachment: fixed;
-    background-position: center center;
-    background-size: cover;
-  }
-}
-
-.nologin {
-  background: unset !important;
-  background-image: url('/images/themes/antarctic/topimage.svg');
-  background-attachment: fixed;
-  background-position: center center;
-  background-size: cover;
-}
-
-.grw-navbar {
-  border-bottom: #ffd700 4px solid;
-}
-
 //== Light Mode
 //
 :root[data-theme='light'] {
@@ -159,14 +137,33 @@
     }
   }
 
-  // login and register
-  .nologin {
-    .nologin-dialog a.link-switch {
-      color: rgba(black, 0.5);
+  .growi:not(.login-page) {
+    // add background-image
+    .page-editor-preview-container {
+      background-image: url('/images/themes/antarctic/bg.svg');
+      background-attachment: fixed;
+      background-position: center center;
+      background-size: cover;
     }
+  }
 
-    .grw-external-auth-form {
-      border-color: #aaa !important;
+  .nologin {
+    .page-wrapper {
+      background-image: url('../images/antarctic/topimage.svg');
+      background-attachment: fixed;
+      background-position: center center;
+      background-size: cover;
+
+      .nologin-dialog .link-switch {
+        color: rgba(black, 0.5);
+      }
+      .grw-external-auth-form {
+        border-color: #aaa;
+      }
     }
   }
+
+  .grw-navbar {
+    border-bottom: #ffd700 4px solid;
+  }
 }

+ 10 - 11
packages/preset-themes/src/styles/christmas.scss

@@ -144,8 +144,6 @@
 
   // login page
   .nologin {
-    background: unset !important;
-
     .input-group {
       .input-group-text {
         color: $gray-700;
@@ -157,16 +155,17 @@
       }
     }
 
-    .nologin-header,
-    .nologin-dialog {
-      background-color: rgba(#ccc, 0.5);
-      a.link-switch {
-        color: #bd3425;
+    .page-wrapper{
+      .nologin-header,
+      .nologin-dialog {
+        background-color: rgba(#ccc, 0.5);
+        a.link-switch {
+          color: #bd3425;
+        }
+      }
+      .grw-external-auth-form {
+        border-color: #aaa;
       }
-    }
-
-    .grw-external-auth-form {
-      border-color: #aaa !important;
     }
   }
 

+ 29 - 29
packages/preset-themes/src/styles/hufflepuff.scss

@@ -145,22 +145,22 @@
 
   // login and register
   .nologin {
-    background: unset !important;
-    background-image: url('../images/hufflepuff/badger-light.png');
-    background-attachment: fixed;
-    background-position: bottom;
-    background-size: cover;
+    .page-wrapper{
+      background-image: url('../images/hufflepuff/badger-light.png');
+      background-attachment: fixed;
+      background-position: bottom;
+      background-size: cover;
 
-    .nologin-header,
-    .nologin-dialog {
-      background-color: rgba(black, 0.1);
-      a.link-switch  {
-        color: #{hsl.darken(var(--color-global),10%)};
+      .nologin-header,
+      .nologin-dialog {
+        background-color: rgba(black, 0.1);
+        a.link-switch  {
+          color: #{hsl.darken(var(--color-global),10%)};
+        }
+      }
+      .grw-external-auth-form {
+        border-color: #993439;
       }
-    }
-
-    .grw-external-auth-form {
-      border-color: #993439 !important;
     }
   }
 
@@ -340,23 +340,23 @@
 
   // login and register
   .nologin {
-    background: unset !important;
-    background-image: url('../images/hufflepuff/badger-light.png');
-    background-attachment: fixed;
-    background-position: bottom;
-    background-size: cover;
-
-    .nologin-header,
-    .nologin-dialog {
-      background-color: rgba(black, 0.1);
-    }
+    .page-wrapper{
+      background-image: url('../images/hufflepuff/badger-light.png');
+      background-attachment: fixed;
+      background-position: bottom;
+      background-size: cover;
+      .nologin-header,
+      .nologin-dialog {
+        background-color: rgba(black, 0.1);
+      }
 
-    .link-switch {
-      color: var(--color-global)!important;
-    }
+      .link-switch {
+        color: var(--color-global);
+      }
 
-    .grw-external-auth-form {
-      border-color: var(--accentcolor) !important;
+      .grw-external-auth-form {
+        border-color: var(--accentcolor);
+      }
     }
   }
 }

+ 15 - 13
packages/preset-themes/src/styles/spring.scss

@@ -136,22 +136,24 @@
 
   // login and register
   .nologin {
-    background: unset !important;
-    background-image: url('../images/spring/spring.svg');
-    background-attachment: fixed;
-    background-position: bottom;
-    background-size: cover;
+    .page-wrapper{
+      background-color: #fff0f5;
+      background-image: url('../images/spring/spring.svg');
+      background-attachment: fixed;
+      background-position: bottom;
+      background-size: cover;
 
-    .nologin-header,
-    .nologin-dialog {
-      background-color: rgba(black, 0.1);
-      a.link-switch {
-        color: var(--color-global);
+      .nologin-header,
+      .nologin-dialog {
+        background-color: rgba(black, 0.1);
+        a.link-switch {
+          color: var(--color-global);
+        }
       }
-    }
 
-    .grw-external-auth-form {
-      border-color: var(--secondary) !important;
+      .grw-external-auth-form {
+        border-color: var(--secondary);
+      }
     }
   }
 

+ 10 - 8
packages/preset-themes/src/styles/wood.scss

@@ -172,16 +172,18 @@
   .nologin {
     background: unset !important;
 
-    .nologin-header,
-    .nologin-dialog {
-      background-color: rgba(black, 0.1);
-      a.link-switch {
-        color: rgba(black, 0.5);
+    .page-wrapper{
+      .nologin-header,
+      .nologin-dialog {
+        background-color: rgba(black, 0.1);
+        a.link-switch {
+          color: rgba(black, 0.5);
+        }
       }
-    }
 
-    .grw-external-auth-form {
-      border-color: #aaa !important;
+      .grw-external-auth-form {
+        border-color: #aaa;
+      }
     }
   }
 

+ 1 - 1
packages/remark-lsx/package.json

@@ -23,7 +23,7 @@
     "@growi/core": "^6.0.6-RC.0",
     "@growi/remark-growi-directive": "^6.0.6-RC.0",
     "@growi/ui": "^6.0.6-RC.0",
-    "swr": "^1.3.0"
+    "swr": "^2.0.3"
   },
   "devDependencies": {
     "eslint-plugin-regex": "^1.8.0",

+ 2 - 2
packages/remark-lsx/src/components/Lsx.tsx

@@ -37,9 +37,9 @@ const LsxSubstance = React.memo(({
     return new LsxContext(prefix, options);
   }, [depth, filter, num, prefix, reverse, sort, except]);
 
-  const { data, error } = useSWRxNodeTree(lsxContext, isImmutable);
+  const { data, error, isLoading: _isLoading } = useSWRxNodeTree(lsxContext, isImmutable);
 
-  const isLoading = data === undefined;
+  const isLoading = _isLoading || data === undefined;
   const hasError = error != null;
   const errorMessage = error?.message;
 

+ 9 - 7
packages/remark-lsx/src/stores/lsx.tsx

@@ -100,7 +100,7 @@ const useSWRxLsxResponse = (
 ): SWRResponse<LsxResponse, Error> => {
   return useSWR(
     ['/_api/lsx', pagePath, options, isImmutable],
-    (endpoint, pagePath, options) => {
+    ([endpoint, pagePath, options]) => {
       return axios.get(endpoint, {
         params: {
           pagePath,
@@ -109,6 +109,7 @@ const useSWRxLsxResponse = (
       }).then(result => result.data as LsxResponse);
     },
     {
+      keepPreviousData: true,
       revalidateIfStale: !isImmutable,
       revalidateOnFocus: !isImmutable,
       revalidateOnReconnect: !isImmutable,
@@ -122,22 +123,23 @@ type LsxNodeTree = {
 }
 
 export const useSWRxNodeTree = (lsxContext: LsxContext, isImmutable?: boolean): SWRResponse<LsxNodeTree, Error> => {
-  const { data, error } = useSWRxLsxResponse(lsxContext.pagePath, lsxContext.options, isImmutable);
-
-  const isLoading = data === undefined && error == null;
+  const {
+    data, error, isLoading, isValidating,
+  } = useSWRxLsxResponse(lsxContext.pagePath, lsxContext.options, isImmutable);
 
   return useSWR(
-    !isLoading ? ['lsxNodeTree', lsxContext.pagePath, lsxContext.options, isImmutable, data, error] : null,
-    (key, pagePath, options, isImmutable, data) => {
+    !isLoading && !isValidating ? ['lsxNodeTree', lsxContext.pagePath, lsxContext.options, isImmutable, data, error] : null,
+    ([, pagePath, , , data]) => {
       if (data === undefined || error != null) {
         throw error;
       }
       return {
-        nodeTree: generatePageNodeTree(pagePath, data.pages),
+        nodeTree: generatePageNodeTree(pagePath, data?.pages),
         toppageViewersCount: data.toppageViewersCount,
       };
     },
     {
+      keepPreviousData: true,
       revalidateIfStale: !isImmutable,
       revalidateOnFocus: !isImmutable,
       revalidateOnReconnect: !isImmutable,

+ 17 - 46
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"
@@ -7126,10 +7121,6 @@ consolidate@^0.16.0:
   dependencies:
     bluebird "^3.7.2"
 
-"consolidated-events@^1.1.0 || ^2.0.0":
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/consolidated-events/-/consolidated-events-2.0.2.tgz#da8d8f8c2b232831413d9e190dc11669c79f4a91"
-
 constant-case@^3.0.3, constant-case@^3.0.4:
   version "3.0.4"
   resolved "https://registry.yarnpkg.com/constant-case/-/constant-case-3.0.4.tgz#3b84a9aeaf4cf31ec45e6bf5de91bdfb0589faf1"
@@ -14440,18 +14431,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 +14514,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 +14997,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"
@@ -18192,16 +18185,6 @@ react-use-ripple@^1.5.2:
   resolved "https://registry.yarnpkg.com/react-use-ripple/-/react-use-ripple-1.5.2.tgz#f42600a0c7729510c3dbba74e0c86ed6c55fd88e"
   integrity sha512-pK7PLEaEGJ4xCM5acxW+ua7ba0lqxbhNzBHzEw+MoD0yVFT3r8SkfkG6aSpiEm4iLZO9HOeSnUz+1k7YVuYX5w==
 
-react-waypoint@^10.1.0:
-  version "10.1.0"
-  resolved "https://registry.yarnpkg.com/react-waypoint/-/react-waypoint-10.1.0.tgz#6ab522a61bd52946260e4a78b3182759a97b40ec"
-  integrity sha512-wiVF0lTslVm27xHbnvUUADUrcDjrQxAp9lEYGExvcoEBScYbXu3Kt++pLrfj6CqOeeRAL4HcX8aANVLSn6bK0Q==
-  dependencies:
-    "@babel/runtime" "^7.12.5"
-    consolidated-events "^1.1.0 || ^2.0.0"
-    prop-types "^15.0.0"
-    react-is "^17.0.1"
-
 react@^18.2.0:
   version "18.2.0"
   resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
@@ -21749,15 +21732,10 @@ swagger2openapi@^5.3.1:
     yaml "^1.3.1"
     yargs "^12.0.5"
 
-swr@^1.3.0:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/swr/-/swr-1.3.0.tgz#c6531866a35b4db37b38b72c45a63171faf9f4e8"
-  integrity sha512-dkghQrOl2ORX9HYrMDtPa7LTVHJjCTeZoB1dqTbnnEDlSvN8JEKpYIYurDfvbQFUUS8Cg8PceFVZNkW0KNNYPw==
-
-swr@^2.0.2:
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/swr/-/swr-2.0.2.tgz#fd34f3aac354f6b70f9134eb4218c747cc899a8d"
-  integrity sha512-iHbQW17hsduonMEliZnr6/yaxb+yvLe2r0+AH+ZfeqKzwc2bb+QRYpZm5/b/H0Lxgy7VWow4o71JeSazSun+9A==
+swr@^2.0.3:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/swr/-/swr-2.0.3.tgz#9fe59a17f55b0fdddccd76b7b2f723f9f8e2263e"
+  integrity sha512-sGvQDok/AHEWTPfhUWXEHBVEXmgGnuahyhmRQbjl9XBYxT/MSlAzvXEKQpyM++bMPaI52vcWS2HiKNaW7+9OFw==
   dependencies:
     use-sync-external-store "^1.2.0"
 
@@ -23069,13 +23047,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"