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

Merge branch 'master' into support/gw7884-dependabot-alert-passport

Shun Miyazawa 3 лет назад
Родитель
Сommit
bd0c4928a9
57 измененных файлов с 590 добавлено и 590 удалено
  1. 1 1
      lerna.json
  2. 1 1
      package.json
  3. 0 5
      packages/app/_obsolete/src/client/services/ContextExtractor.tsx
  4. 2 2
      packages/app/next.config.js
  5. 7 7
      packages/app/package.json
  6. 0 11
      packages/app/src/client/services/AdminCustomizeContainer.js
  7. 0 15
      packages/app/src/components/Admin/Customize/CustomizeFunctionSetting.tsx
  8. 5 2
      packages/app/src/components/DescendantsPageList.tsx
  9. 21 15
      packages/app/src/components/Layout/BasicLayout.tsx
  10. 10 7
      packages/app/src/components/Layout/RawLayout.tsx
  11. 4 2
      packages/app/src/components/LoginForm.tsx
  12. 4 3
      packages/app/src/components/PageAccessoriesModal.tsx
  13. 4 4
      packages/app/src/components/PageEditor/OptionsSelector.tsx
  14. 16 12
      packages/app/src/components/PageHistory.tsx
  15. 5 0
      packages/app/src/components/PageHistory/PageRevisionTable.module.scss
  16. 62 86
      packages/app/src/components/PageHistory/PageRevisionTable.tsx
  17. 0 88
      packages/app/src/components/PageHistory/Revision.jsx
  18. 13 0
      packages/app/src/components/PageHistory/Revision.module.scss
  19. 77 0
      packages/app/src/components/PageHistory/Revision.tsx
  20. 0 87
      packages/app/src/components/PageHistory/RevisionDiff.jsx
  21. 35 0
      packages/app/src/components/PageHistory/RevisionDiff.module.scss
  22. 67 0
      packages/app/src/components/PageHistory/RevisionDiff.tsx
  23. 1 0
      packages/app/src/components/PrivateLegacyPages.tsx
  24. 14 0
      packages/app/src/components/RevisionComparer/RevisionComparer.module.scss
  25. 21 33
      packages/app/src/components/RevisionComparer/RevisionComparer.tsx
  26. 3 3
      packages/app/src/components/SearchPage.tsx
  27. 0 0
      packages/app/src/components/UnsavedAlertDialog.tsx
  28. 35 0
      packages/app/src/migrations/20221014130200-remove-customize-is-saved-states-of-tab-changes.js
  29. 28 21
      packages/app/src/pages/[[...path]].page.tsx
  30. 1 8
      packages/app/src/pages/_app.page.tsx
  31. 9 7
      packages/app/src/pages/_private-legacy-pages.page.tsx
  32. 17 9
      packages/app/src/pages/_search.page.tsx
  33. 2 2
      packages/app/src/pages/invited.page.tsx
  34. 1 1
      packages/app/src/pages/login.page.tsx
  35. 2 4
      packages/app/src/pages/maintenance.page.tsx
  36. 9 7
      packages/app/src/pages/tags.page.tsx
  37. 14 8
      packages/app/src/pages/trash.page.tsx
  38. 6 5
      packages/app/src/server/crowi/index.js
  39. 0 3
      packages/app/src/server/models/config.ts
  40. 0 6
      packages/app/src/server/routes/apiv3/customize-setting.js
  41. 5 1
      packages/app/src/stores/context.tsx
  42. 4 5
      packages/app/src/stores/editor.tsx
  43. 17 7
      packages/app/src/stores/page-listing.tsx
  44. 1 1
      packages/app/src/stores/page.tsx
  45. 34 26
      packages/app/src/stores/ui.tsx
  46. 0 68
      packages/app/src/styles/_page-history.scss
  47. 2 2
      packages/app/src/styles/_page.scss
  48. 1 1
      packages/codemirror-textlint/package.json
  49. 1 1
      packages/core/package.json
  50. 7 1
      packages/core/src/interfaces/revision.ts
  51. 1 1
      packages/plugin-attachment-refs/package.json
  52. 4 4
      packages/plugin-lsx/package.json
  53. 1 1
      packages/remark-growi-plugin/package.json
  54. 1 1
      packages/slack/package.json
  55. 2 2
      packages/slackbot-proxy/package.json
  56. 2 2
      packages/ui/package.json
  57. 10 1
      packages/ui/src/utils/browser-utils.ts

+ 1 - 1
lerna.json

@@ -1,7 +1,7 @@
 {
 {
   "npmClient": "yarn",
   "npmClient": "yarn",
   "useWorkspaces": true,
   "useWorkspaces": true,
-  "version": "6.0.0-RC.1",
+  "version": "6.0.0-RC.3",
   "packages": [
   "packages": [
     "packages/*"
     "packages/*"
   ]
   ]

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "growi",
   "name": "growi",
-  "version": "6.0.0-RC.1",
+  "version": "6.0.0-RC.3",
   "description": "Team collaboration software using markdown",
   "description": "Team collaboration software using markdown",
   "tags": [
   "tags": [
     "wiki",
     "wiki",

+ 0 - 5
packages/app/_obsolete/src/client/services/ContextExtractor.tsx

@@ -167,11 +167,6 @@ const ContextExtractorOnce: FC = () => {
   usePreferDrawerModeOnEditByUser();
   usePreferDrawerModeOnEditByUser();
   useIsDeviceSmallerThanMd();
   useIsDeviceSmallerThanMd();
 
 
-  // Navigation
-  usePreferDrawerModeByUser();
-  usePreferDrawerModeOnEditByUser();
-  useIsDeviceSmallerThanMd();
-
   // Editor
   // Editor
   // useSelectedGrant(grant);
   // useSelectedGrant(grant);
   // useSelectedGrantGroupId(grantGroupId);
   // useSelectedGrantGroupId(grantGroupId);

+ 2 - 2
packages/app/next.config.js

@@ -46,8 +46,8 @@ const setupTranspileModules = () => {
     ...listPrefixedPackages(['remark-', 'rehype-', 'hast-', 'mdast-', 'micromark-', 'unist-']),
     ...listPrefixedPackages(['remark-', 'rehype-', 'hast-', 'mdast-', 'micromark-', 'unist-']),
   ];
   ];
 
 
-  logger.info('{bold:Listing scoped packages for transpiling:}');
-  logger.unprefixed('info', `{grey:${JSON.stringify(packages, null, 2)}}`);
+  // logger.info('{bold:Listing scoped packages for transpiling:}');
+  // logger.unprefixed('info', `{grey:${JSON.stringify(packages, null, 2)}}`);
 
 
   return require('next-transpile-modules')(packages);
   return require('next-transpile-modules')(packages);
 };
 };

+ 7 - 7
packages/app/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/app",
   "name": "@growi/app",
-  "version": "6.0.0-RC.1",
+  "version": "6.0.0-RC.3",
   "license": "MIT",
   "license": "MIT",
   "scripts": {
   "scripts": {
     "//// for production": "",
     "//// for production": "",
@@ -64,11 +64,11 @@
     "@elastic/elasticsearch7": "npm:@elastic/elasticsearch@^7.17.0",
     "@elastic/elasticsearch7": "npm:@elastic/elasticsearch@^7.17.0",
     "@godaddy/terminus": "^4.9.0",
     "@godaddy/terminus": "^4.9.0",
     "@google-cloud/storage": "^5.8.5",
     "@google-cloud/storage": "^5.8.5",
-    "@growi/codemirror-textlint": "^6.0.0-RC.1",
-    "@growi/core": "^6.0.0-RC.1",
-    "@growi/plugin-attachment-refs": "^6.0.0-RC.1",
-    "@growi/plugin-lsx": "^6.0.0-RC.1",
-    "@growi/slack": "^6.0.0-RC.1",
+    "@growi/codemirror-textlint": "^6.0.0-RC.3",
+    "@growi/core": "^6.0.0-RC.3",
+    "@growi/plugin-attachment-refs": "^6.0.0-RC.3",
+    "@growi/plugin-lsx": "^6.0.0-RC.3",
+    "@growi/slack": "^6.0.0-RC.3",
     "@promster/express": "^7.0.2",
     "@promster/express": "^7.0.2",
     "@promster/server": "^7.0.4",
     "@promster/server": "^7.0.4",
     "@slack/events-api": "^3.0.0",
     "@slack/events-api": "^3.0.0",
@@ -203,7 +203,7 @@
   },
   },
   "devDependencies": {
   "devDependencies": {
     "@alienfast/i18next-loader": "^1.1.4",
     "@alienfast/i18next-loader": "^1.1.4",
-    "@growi/ui": "^6.0.0-RC.1",
+    "@growi/ui": "^6.0.0-RC.3",
     "@handsontable/react": "=2.1.0",
     "@handsontable/react": "=2.1.0",
     "@icon/themify-icons": "1.0.1-alpha.3",
     "@icon/themify-icons": "1.0.1-alpha.3",
     "@next/bundle-analyzer": "^12.2.3",
     "@next/bundle-analyzer": "^12.2.3",

+ 0 - 11
packages/app/src/client/services/AdminCustomizeContainer.js

@@ -25,7 +25,6 @@ export default class AdminCustomizeContainer extends Container {
     this.state = {
     this.state = {
       retrieveError: null,
       retrieveError: null,
       isEnabledTimeline: false,
       isEnabledTimeline: false,
-      isSavedStatesOfTabChanges: false,
       isEnabledAttachTitleHeader: false,
       isEnabledAttachTitleHeader: false,
 
 
       pageLimitationS: null,
       pageLimitationS: null,
@@ -81,7 +80,6 @@ export default class AdminCustomizeContainer extends Container {
 
 
       this.setState({
       this.setState({
         isEnabledTimeline: customizeParams.isEnabledTimeline,
         isEnabledTimeline: customizeParams.isEnabledTimeline,
-        isSavedStatesOfTabChanges: customizeParams.isSavedStatesOfTabChanges,
         isEnabledAttachTitleHeader: customizeParams.isEnabledAttachTitleHeader,
         isEnabledAttachTitleHeader: customizeParams.isEnabledAttachTitleHeader,
         pageLimitationS: customizeParams.pageLimitationS,
         pageLimitationS: customizeParams.pageLimitationS,
         pageLimitationM: customizeParams.pageLimitationM,
         pageLimitationM: customizeParams.pageLimitationM,
@@ -116,13 +114,6 @@ export default class AdminCustomizeContainer extends Container {
     this.setState({ isEnabledTimeline:  !this.state.isEnabledTimeline });
     this.setState({ isEnabledTimeline:  !this.state.isEnabledTimeline });
   }
   }
 
 
-  /**
-   * Switch savedStatesOfTabChanges
-   */
-  switchSavedStatesOfTabChanges() {
-    this.setState({ isSavedStatesOfTabChanges:  !this.state.isSavedStatesOfTabChanges });
-  }
-
   /**
   /**
    * Switch enabledAttachTitleHeader
    * Switch enabledAttachTitleHeader
    */
    */
@@ -247,7 +238,6 @@ export default class AdminCustomizeContainer extends Container {
     try {
     try {
       const response = await apiv3Put('/customize-setting/function', {
       const response = await apiv3Put('/customize-setting/function', {
         isEnabledTimeline: this.state.isEnabledTimeline,
         isEnabledTimeline: this.state.isEnabledTimeline,
-        isSavedStatesOfTabChanges: this.state.isSavedStatesOfTabChanges,
         isEnabledAttachTitleHeader: this.state.isEnabledAttachTitleHeader,
         isEnabledAttachTitleHeader: this.state.isEnabledAttachTitleHeader,
         pageLimitationS: this.state.pageLimitationS,
         pageLimitationS: this.state.pageLimitationS,
         pageLimitationM: this.state.pageLimitationM,
         pageLimitationM: this.state.pageLimitationM,
@@ -260,7 +250,6 @@ export default class AdminCustomizeContainer extends Container {
       const { customizedParams } = response.data;
       const { customizedParams } = response.data;
       this.setState({
       this.setState({
         isEnabledTimeline: customizedParams.isEnabledTimeline,
         isEnabledTimeline: customizedParams.isEnabledTimeline,
-        isSavedStatesOfTabChanges: customizedParams.isSavedStatesOfTabChanges,
         isEnabledAttachTitleHeader: customizedParams.isEnabledAttachTitleHeader,
         isEnabledAttachTitleHeader: customizedParams.isEnabledAttachTitleHeader,
         pageLimitationS: customizedParams.pageLimitationS,
         pageLimitationS: customizedParams.pageLimitationS,
         pageLimitationM: customizedParams.pageLimitationM,
         pageLimitationM: customizedParams.pageLimitationM,

+ 0 - 15
packages/app/src/components/Admin/Customize/CustomizeFunctionSetting.tsx

@@ -44,21 +44,6 @@ const CustomizeFunctionSetting = (props: Props): JSX.Element => {
           </Card>
           </Card>
 
 
 
 
-          <div className="form-group row">
-            <div className="offset-md-3 col-md-6 text-left">
-              <CustomizeFunctionOption
-                optionId="isSavedStatesOfTabChanges"
-                label={t('admin:customize_settings.function_options.tab_switch')}
-                isChecked={adminCustomizeContainer.state.isSavedStatesOfTabChanges}
-                onChecked={() => { adminCustomizeContainer.switchSavedStatesOfTabChanges() }}
-              >
-                <p className="form-text text-muted">
-                  {t('admin:customize_settings.function_options.tab_switch_desc1')}<br />
-                  {t('admin:customize_settings.function_options.tab_switch_desc2')}
-                </p>
-              </CustomizeFunctionOption>
-            </div>
-          </div>
           <div className="form-group row">
           <div className="form-group row">
             <div className="offset-md-3 col-md-6 text-left">
             <div className="offset-md-3 col-md-6 text-left">
               <CustomizeFunctionOption
               <CustomizeFunctionOption

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

@@ -10,7 +10,9 @@ import {
 } from '~/interfaces/page';
 } from '~/interfaces/page';
 import { IPagingResult } from '~/interfaces/paging-result';
 import { IPagingResult } from '~/interfaces/paging-result';
 import { OnDeletedFunction, OnPutBackedFunction } from '~/interfaces/ui';
 import { OnDeletedFunction, OnPutBackedFunction } from '~/interfaces/ui';
-import { useIsGuestUser, useIsSharedUser, useIsTrashPage } from '~/stores/context';
+import {
+  useIsGuestUser, useIsSharedUser, useIsTrashPage, useShowPageLimitationXL,
+} from '~/stores/context';
 import {
 import {
   usePageTreeTermManager, useDescendantsPageListForCurrentPathTermManager, useSWRxDescendantsPageListForCurrrentPath,
   usePageTreeTermManager, useDescendantsPageListForCurrentPathTermManager, useSWRxDescendantsPageListForCurrrentPath,
   useSWRxPageInfoForList, useSWRxPageList,
   useSWRxPageInfoForList, useSWRxPageList,
@@ -167,7 +169,8 @@ export const DescendantsPageListForCurrentPath = (): JSX.Element => {
   const [activePage, setActivePage] = useState(1);
   const [activePage, setActivePage] = useState(1);
 
 
   const { data: isTrashPage } = useIsTrashPage();
   const { data: isTrashPage } = useIsTrashPage();
-  const { data: pagingResult, error, mutate } = useSWRxDescendantsPageListForCurrrentPath(activePage);
+  const { data: limit } = useShowPageLimitationXL();
+  const { data: pagingResult, error, mutate } = useSWRxDescendantsPageListForCurrrentPath(activePage, limit);
 
 
   if (error != null) {
   if (error != null) {
     return (
     return (

+ 21 - 15
packages/app/src/components/Layout/BasicLayout.tsx

@@ -1,6 +1,8 @@
 import React, { ReactNode } from 'react';
 import React, { ReactNode } from 'react';
 
 
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
+import { DndProvider } from 'react-dnd';
+import { HTML5Backend } from 'react-dnd-html5-backend';
 
 
 import { GrowiNavbar } from '../Navbar/GrowiNavbar';
 import { GrowiNavbar } from '../Navbar/GrowiNavbar';
 import Sidebar from '../Sidebar';
 import Sidebar from '../Sidebar';
@@ -39,27 +41,31 @@ export const BasicLayout = ({
 
 
   return (
   return (
     <RawLayout title={title} className={myClassName}>
     <RawLayout title={title} className={myClassName}>
-      <GrowiNavbar />
 
 
-      <div className="page-wrapper d-flex d-print-block">
-        <div className="grw-sidebar-wrapper">
-          <Sidebar />
-        </div>
+      <DndProvider backend={HTML5Backend}>
+        <GrowiNavbar />
+
+        <div className="page-wrapper d-flex d-print-block">
+          <div className="grw-sidebar-wrapper">
+            <Sidebar />
+          </div>
 
 
-        <div className="flex-fill mw-0" style={{ position: 'relative' }}>
-          {children}
+          <div className="flex-fill mw-0" style={{ position: 'relative' }}>
+            {children}
+          </div>
         </div>
         </div>
-      </div>
 
 
-      <GrowiNavbarBottom />
+        <GrowiNavbarBottom />
+
+        <PageCreateModal />
+        <PageDuplicateModal />
+        <PageDeleteModal />
+        <PageRenameModal />
+        <PageAccessoriesModal />
+        <DrawioModal />
+      </DndProvider>
 
 
-      <PageCreateModal />
-      <PageDuplicateModal />
-      <PageDeleteModal />
-      <PageRenameModal />
       <PagePresentationModal />
       <PagePresentationModal />
-      <PageAccessoriesModal />
-      <DrawioModal />
       <HotkeysManager />
       <HotkeysManager />
 
 
       <Fab />
       <Fab />

+ 10 - 7
packages/app/src/components/Layout/RawLayout.tsx

@@ -4,10 +4,11 @@ import Head from 'next/head';
 import { useIsomorphicLayoutEffect } from 'usehooks-ts';
 import { useIsomorphicLayoutEffect } from 'usehooks-ts';
 
 
 import { useGrowiTheme } from '~/stores/context';
 import { useGrowiTheme } from '~/stores/context';
-import { ColorScheme, useNextThemes } from '~/stores/use-next-themes';
+import { ColorScheme, useNextThemes, NextThemesProvider } from '~/stores/use-next-themes';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
-import { ThemeProvider } from '../Theme/utils/ThemeProvider';
+
+import { ThemeProvider as GrowiThemeProvider } from '../Theme/utils/ThemeProvider';
 
 
 
 
 const logger = loggerFactory('growi:cli:RawLayout');
 const logger = loggerFactory('growi:cli:RawLayout');
@@ -43,11 +44,13 @@ export const RawLayout = ({ children, title, className }: Props): JSX.Element =>
         <meta charSet="utf-8" />
         <meta charSet="utf-8" />
         <meta name="viewport" content="initial-scale=1.0, width=device-width" />
         <meta name="viewport" content="initial-scale=1.0, width=device-width" />
       </Head>
       </Head>
-      <ThemeProvider theme={growiTheme} colorScheme={colorScheme}>
-        <div className={classNames.join(' ')} data-color-scheme={colorScheme}>
-          {children}
-        </div>
-      </ThemeProvider>
+      <NextThemesProvider>
+        <GrowiThemeProvider theme={growiTheme} colorScheme={colorScheme}>
+          <div className={classNames.join(' ')} data-color-scheme={colorScheme}>
+            {children}
+          </div>
+        </GrowiThemeProvider>
+      </NextThemesProvider>
     </>
     </>
   );
   );
 };
 };

+ 4 - 2
packages/app/src/components/LoginForm.tsx

@@ -9,6 +9,7 @@ import ReactCardFlip from 'react-card-flip';
 import { apiv3Post } from '~/client/util/apiv3-client';
 import { apiv3Post } from '~/client/util/apiv3-client';
 import { LoginErrorCode } from '~/interfaces/errors/login-error';
 import { LoginErrorCode } from '~/interfaces/errors/login-error';
 import { IErrorV3 } from '~/interfaces/errors/v3-error';
 import { IErrorV3 } from '~/interfaces/errors/v3-error';
+import { toArrayIfNot } from '~/utils/array-utils';
 
 
 type LoginFormProps = {
 type LoginFormProps = {
   username?: string,
   username?: string,
@@ -80,10 +81,11 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
     try {
     try {
       const res = await apiv3Post('/login', { loginForm });
       const res = await apiv3Post('/login', { loginForm });
       const { redirectTo } = res.data;
       const { redirectTo } = res.data;
-      router.push(redirectTo);
+      router.push(redirectTo ?? '/');
     }
     }
     catch (err) {
     catch (err) {
-      setLoginErrors(err);
+      const errs = toArrayIfNot(err);
+      setLoginErrors(errs);
     }
     }
     return;
     return;
 
 

+ 4 - 3
packages/app/src/components/PageAccessoriesModal.tsx

@@ -5,8 +5,9 @@ import {
   Modal, ModalBody, ModalHeader,
   Modal, ModalBody, ModalHeader,
 } from 'reactstrap';
 } from 'reactstrap';
 
 
-
-import { useDisableLinkSharing, useIsGuestUser, useIsSharedUser } from '~/stores/context';
+import {
+  useDisableLinkSharing, useIsGuestUser, useIsSharedUser,
+} from '~/stores/context';
 import { usePageAccessoriesModal, PageAccessoriesModalContents } from '~/stores/modal';
 import { usePageAccessoriesModal, PageAccessoriesModalContents } from '~/stores/modal';
 
 
 import { CustomNavTab } from './CustomNavigation/CustomNav';
 import { CustomNavTab } from './CustomNavigation/CustomNav';
@@ -16,7 +17,7 @@ import AttachmentIcon from './Icons/AttachmentIcon';
 import HistoryIcon from './Icons/HistoryIcon';
 import HistoryIcon from './Icons/HistoryIcon';
 import ShareLinkIcon from './Icons/ShareLinkIcon';
 import ShareLinkIcon from './Icons/ShareLinkIcon';
 import PageAttachment from './PageAttachment';
 import PageAttachment from './PageAttachment';
-import PageHistory from './PageHistory';
+import { PageHistory } from './PageHistory';
 import ShareLink from './ShareLink/ShareLink';
 import ShareLink from './ShareLink/ShareLink';
 
 
 import styles from './PageAccessoriesModal.module.scss';
 import styles from './PageAccessoriesModal.module.scss';

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

@@ -184,7 +184,7 @@ const ConfigurationDropdown = memo(({ onConfirmEnableTextlint }: ConfigurationDr
 
 
     const iconClasses = ['text-info'];
     const iconClasses = ['text-info'];
     if (isActive) {
     if (isActive) {
-      iconClasses.push('ti-check');
+      iconClasses.push('ti ti-check');
     }
     }
     const iconClassName = iconClasses.join(' ');
     const iconClassName = iconClasses.join(' ');
 
 
@@ -208,7 +208,7 @@ const ConfigurationDropdown = memo(({ onConfirmEnableTextlint }: ConfigurationDr
 
 
     const iconClasses = ['text-info'];
     const iconClasses = ['text-info'];
     if (isActive) {
     if (isActive) {
-      iconClasses.push('ti-check');
+      iconClasses.push('ti ti-check');
     }
     }
     const iconClassName = iconClasses.join(' ');
     const iconClassName = iconClasses.join(' ');
 
 
@@ -232,7 +232,7 @@ const ConfigurationDropdown = memo(({ onConfirmEnableTextlint }: ConfigurationDr
 
 
     const iconClasses = ['text-info'];
     const iconClasses = ['text-info'];
     if (isActive) {
     if (isActive) {
-      iconClasses.push('ti-check');
+      iconClasses.push('ti ti-check');
     }
     }
     const iconClassName = iconClasses.join(' ');
     const iconClassName = iconClasses.join(' ');
 
 
@@ -270,7 +270,7 @@ const ConfigurationDropdown = memo(({ onConfirmEnableTextlint }: ConfigurationDr
 
 
     const iconClasses = ['text-info'];
     const iconClasses = ['text-info'];
     if (isTextlintEnabled) {
     if (isTextlintEnabled) {
-      iconClasses.push('ti-check');
+      iconClasses.push('ti ti-check');
     }
     }
     const iconClassName = iconClasses.join(' ');
     const iconClassName = iconClasses.join(' ');
 
 

+ 16 - 12
packages/app/src/components/PageHistory.jsx → packages/app/src/components/PageHistory.tsx

@@ -1,21 +1,27 @@
 import React, { useState, useEffect } from 'react';
 import React, { useState, useEffect } from 'react';
 
 
+import { IRevisionHasPageId } from '@growi/core';
+
 import { useCurrentPageId } from '~/stores/context';
 import { useCurrentPageId } from '~/stores/context';
 import { useSWRxPageRevisions } from '~/stores/page';
 import { useSWRxPageRevisions } from '~/stores/page';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
-import PageRevisionTable from './PageHistory/PageRevisionTable';
+import { PageRevisionTable } from './PageHistory/PageRevisionTable';
 import PaginationWrapper from './PaginationWrapper';
 import PaginationWrapper from './PaginationWrapper';
-import RevisionComparer from './RevisionComparer/RevisionComparer';
+import { RevisionComparer } from './RevisionComparer/RevisionComparer';
 
 
 const logger = loggerFactory('growi:PageHistory');
 const logger = loggerFactory('growi:PageHistory');
 
 
-const PageHistory = () => {
+export const PageHistory = (): JSX.Element => {
+
   const [activePage, setActivePage] = useState(1);
   const [activePage, setActivePage] = useState(1);
+
   const { data: currentPageId } = useCurrentPageId();
   const { data: currentPageId } = useCurrentPageId();
-  const { data: revisionsData } = useSWRxPageRevisions(currentPageId, activePage, 10);
-  const [sourceRevision, setSourceRevision] = useState(null);
-  const [targetRevision, setTargetRevision] = useState(null);
+
+  const { data: revisionsData } = useSWRxPageRevisions(activePage, 10, currentPageId);
+
+  const [sourceRevision, setSourceRevision] = useState<IRevisionHasPageId>();
+  const [targetRevision, setTargetRevision] = useState<IRevisionHasPageId>();
 
 
   useEffect(() => {
   useEffect(() => {
     if (revisionsData != null) {
     if (revisionsData != null) {
@@ -24,17 +30,17 @@ const PageHistory = () => {
     }
     }
   }, [revisionsData]);
   }, [revisionsData]);
 
 
-
   const pagingLimit = 10;
   const pagingLimit = 10;
 
 
-  if (revisionsData == null) {
+  if (revisionsData == null || sourceRevision == null || targetRevision == null || currentPageId == null) {
     return (
     return (
       <div className="text-muted text-center">
       <div className="text-muted text-center">
         <i className="fa fa-2x fa-spinner fa-pulse mt-3"></i>
         <i className="fa fa-2x fa-spinner fa-pulse mt-3"></i>
       </div>
       </div>
     );
     );
   }
   }
-  function pager() {
+
+  const pager = () => {
     return (
     return (
       <PaginationWrapper
       <PaginationWrapper
         activePage={activePage}
         activePage={activePage}
@@ -44,7 +50,7 @@ const PageHistory = () => {
         align="center"
         align="center"
       />
       />
     );
     );
-  }
+  };
 
 
   return (
   return (
     <div className="revision-history" data-testid="page-history">
     <div className="revision-history" data-testid="page-history">
@@ -67,5 +73,3 @@ const PageHistory = () => {
     </div>
     </div>
   );
   );
 };
 };
-
-export default PageHistory;

+ 5 - 0
packages/app/src/components/PageHistory/PageRevisionTable.module.scss

@@ -0,0 +1,5 @@
+.revision-history-table :global {
+  tbody {
+    max-height: 250px;
+  }
+}

+ 62 - 86
packages/app/src/components/PageHistory/PageRevisionTable.jsx → packages/app/src/components/PageHistory/PageRevisionTable.tsx

@@ -1,23 +1,35 @@
 import React from 'react';
 import React from 'react';
 
 
+import { IRevisionHasId } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
-import PropTypes from 'prop-types';
-
-import Revision from './Revision';
-
-class PageRevisionTable extends React.Component {
-
-  /**
-   * render a row (Revision component and RevisionDiff component)
-   * @param {Revison} revision
-   * @param {Revision} previousRevision
-   * @param {boolean} hasDiff whether revision has difference to previousRevision
-   * @param {boolean} isContiguousNodiff true if the current 'hasDiff' and one of previous row is both false
-   */
-  renderRow(revision, previousRevision, latestRevision, isOldestRevision, hasDiff) {
-    const {
-      t, sourceRevision, targetRevision, onChangeSourceInvoked, onChangeTargetInvoked,
-    } = this.props;
+
+import { Revision } from './Revision';
+
+import styles from './PageRevisionTable.module.scss';
+
+type PageRevisionTAble = {
+  revisions: IRevisionHasId[],
+  pagingLimit: number,
+  sourceRevision: IRevisionHasId,
+  targetRevision: IRevisionHasId,
+  onChangeSourceInvoked: React.Dispatch<React.SetStateAction<IRevisionHasId | undefined>>,
+  onChangeTargetInvoked: React.Dispatch<React.SetStateAction<IRevisionHasId | undefined>>,
+}
+
+export const PageRevisionTable = (props: PageRevisionTAble): JSX.Element => {
+  const { t } = useTranslation();
+
+  const {
+    revisions, pagingLimit, sourceRevision, targetRevision, onChangeSourceInvoked, onChangeTargetInvoked,
+  } = props;
+
+  const revisionCount = revisions.length;
+  const latestRevision = revisions[0];
+  const oldestRevision = revisions[revisions.length - 1];
+
+  const renderRow = (revision: IRevisionHasId, previousRevision: IRevisionHasId, latestRevision: IRevisionHasId,
+      isOldestRevision: boolean, hasDiff: boolean) => {
+
     const revisionId = revision._id;
     const revisionId = revision._id;
 
 
     const handleCompareLatestRevisionButton = () => {
     const handleCompareLatestRevisionButton = () => {
@@ -35,7 +47,6 @@ class PageRevisionTable extends React.Component {
         <td className="col" key={`revision-history-top-${revisionId}`}>
         <td className="col" key={`revision-history-top-${revisionId}`}>
           <div className="d-lg-flex">
           <div className="d-lg-flex">
             <Revision
             <Revision
-              t={this.props.t}
               revision={revision}
               revision={revision}
               isLatestRevision={revision === latestRevision}
               isLatestRevision={revision === latestRevision}
               hasDiff={hasDiff}
               hasDiff={hasDiff}
@@ -98,73 +109,38 @@ class PageRevisionTable extends React.Component {
         </td>
         </td>
       </tr>
       </tr>
     );
     );
-  }
-
-  render() {
-    const { t, pagingLimit } = this.props;
-
-    const revisions = this.props.revisions;
-    const revisionCount = this.props.revisions.length;
-    const latestRevision = revisions[0];
-    const oldestRevision = revisions[revisions.length - 1];
-
-    let hasDiffPrev;
-
-    const revisionList = this.props.revisions.map((revision, idx) => {
-      // Returns null because the last revision is for the bottom diff display
-      if (idx === pagingLimit) {
-        return null;
-      }
-
-      let previousRevision;
-      if (idx + 1 < revisionCount) {
-        previousRevision = revisions[idx + 1];
-      }
-      else {
-        previousRevision = revision; // if it is the first revision, show full text as diff text
-      }
+  };
+
+  const revisionList = revisions.map((revision, idx) => {
+    // Returns null because the last revision is for the bottom diff display
+    if (idx === pagingLimit) {
+      return null;
+    }
+
+    // if it is the first revision, show full text as diff text
+    const previousRevision = (idx + 1 < revisionCount) ? revisions[idx + 1] : revision;
+
+    const isOldestRevision = revision === oldestRevision;
+
+    // set 'true' if undefined for backward compatibility
+    const hasDiff = revision.hasDiffToPrev !== false;
+
+    return renderRow(revision, previousRevision, latestRevision, isOldestRevision, hasDiff);
+  });
+
+  return (
+    <table className={`${styles['revision-history-table']} table revision-history-table`}>
+      <thead>
+        <tr className="d-flex">
+          <th className="col">{ t('page_history.revision') }</th>
+          <th className="col-1">{ t('page_history.comparing_source') }</th>
+          <th className="col-2">{ t('page_history.comparing_target') }</th>
+        </tr>
+      </thead>
+      <tbody className="overflow-auto d-block">
+        {revisionList}
+      </tbody>
+    </table>
+  );
 
 
-      const isOldestRevision = revision === oldestRevision;
-
-      const hasDiff = revision.hasDiffToPrev !== false; // set 'true' if undefined for backward compatibility
-
-      hasDiffPrev = hasDiff;
-
-      return this.renderRow(revision, previousRevision, latestRevision, isOldestRevision, hasDiff);
-    });
-
-    return (
-      <table className="table revision-history-table">
-        <thead>
-          <tr className="d-flex">
-            <th className="col">{ t('page_history.revision') }</th>
-            <th className="col-1">{ t('page_history.comparing_source') }</th>
-            <th className="col-2">{ t('page_history.comparing_target') }</th>
-          </tr>
-        </thead>
-        <tbody className="overflow-auto d-block">
-          {revisionList}
-        </tbody>
-      </table>
-    );
-  }
-
-}
-
-PageRevisionTable.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-
-  revisions: PropTypes.array,
-  pagingLimit: PropTypes.number,
-  sourceRevision: PropTypes.instanceOf(Object),
-  targetRevision: PropTypes.instanceOf(Object),
-  onChangeSourceInvoked: PropTypes.func.isRequired,
-  onChangeTargetInvoked: PropTypes.func.isRequired,
 };
 };
-
-const PageRevisionTableWrapperFC = (props) => {
-  const { t } = useTranslation();
-  return <PageRevisionTable t={t} {...props} />;
-};
-
-export default PageRevisionTableWrapperFC;

+ 0 - 88
packages/app/src/components/PageHistory/Revision.jsx

@@ -1,88 +0,0 @@
-import React from 'react';
-
-import { UserPicture } from '@growi/ui';
-import PropTypes from 'prop-types';
-
-import UserDate from '../User/UserDate';
-import Username from '../User/Username';
-
-export default class Revision extends React.Component {
-
-  componentDidMount() {
-  }
-
-  renderSimplifiedNodiff(revision) {
-    const { t } = this.props;
-
-    const author = revision.author;
-
-    let pic = '';
-    if (typeof author === 'object') {
-      pic = <UserPicture user={author} size="sm" />;
-    }
-
-    return (
-      <div className="revision-history-main revision-history-main-nodiff my-1 d-flex align-items-center">
-        <div className="picture-container">
-          {pic}
-        </div>
-        <div className="ml-3">
-          <span className="text-muted small">
-            <UserDate dateTime={revision.createdAt} /> ({ t('No diff') })
-          </span>
-        </div>
-      </div>
-    );
-  }
-
-  renderFull(revision) {
-    const { t } = this.props;
-
-    const author = revision.author;
-
-    let pic = '';
-    if (typeof author === 'object') {
-      pic = <UserPicture user={author} size="lg" />;
-    }
-
-    return (
-      <div className="revision-history-main d-flex">
-        <div className="picture-container">
-          {pic}
-        </div>
-        <div className="ml-2">
-          <div className="revision-history-author mb-1">
-            <strong><Username user={author}></Username></strong>
-            {this.props.isLatestRevision && <span className="badge badge-info ml-2">Latest</span>}
-          </div>
-          <div className="mb-1">
-            <UserDate dateTime={revision.createdAt} />
-            <br className="d-xl-none d-block" />
-            <a className="ml-xl-3" href={`?revisionId=${revision._id}`}>
-              <i className="icon-login"></i> { t('Go to this version') }
-            </a>
-          </div>
-        </div>
-      </div>
-    );
-  }
-
-  render() {
-    const revision = this.props.revision;
-
-    if (!this.props.hasDiff) {
-      return this.renderSimplifiedNodiff(revision);
-    }
-
-    return this.renderFull(revision);
-
-  }
-
-}
-
-Revision.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  revision: PropTypes.object,
-  isLatestRevision: PropTypes.bool.isRequired,
-  hasDiff: PropTypes.bool.isRequired,
-};

+ 13 - 0
packages/app/src/components/PageHistory/Revision.module.scss

@@ -0,0 +1,13 @@
+.revision-history-main :global {
+  img.picture-lg {
+    width: 32px;
+    height: 32px;
+  }
+}
+
+.revision-history-main-nodiff :global {
+  .picture-container {
+    min-width: 32px;
+    text-align: center; // centering .picture
+  }
+}

+ 77 - 0
packages/app/src/components/PageHistory/Revision.tsx

@@ -0,0 +1,77 @@
+import React from 'react';
+
+import { IRevisionHasId } from '@growi/core';
+import { UserPicture } from '@growi/ui';
+import { useTranslation } from 'next-i18next';
+
+import UserDate from '../User/UserDate';
+import Username from '../User/Username';
+
+import styles from './Revision.module.scss';
+
+type RevisionProps = {
+  revision: IRevisionHasId,
+  isLatestRevision: boolean,
+  hasDiff: boolean,
+}
+
+export const Revision = (props: RevisionProps): JSX.Element => {
+  const { t } = useTranslation();
+
+  const { revision, isLatestRevision, hasDiff } = props;
+
+  const renderSimplifiedNodiff = (revision: IRevisionHasId) => {
+
+    const author = revision.author;
+
+    const pic = (typeof author === 'object') ? <UserPicture user={author} size="sm" /> : <></>;
+
+    return (
+      <div className={`${styles['revision-history-main']} ${styles['revision-history-main-nodiff']}
+        revision-history-main revision-history-main-nodiff my-1 d-flex align-items-center`}>
+        <div className="picture-container">
+          { pic }
+        </div>
+        <div className="ml-3">
+          <span className="text-muted small">
+            <UserDate dateTime={revision.createdAt} /> ({ t('No diff') })
+          </span>
+        </div>
+      </div>
+    );
+  };
+
+  const renderFull = (revision: IRevisionHasId) => {
+
+    const author = revision.author;
+
+    const pic = (typeof author === 'object') ? <UserPicture user={author} size="lg" /> : <></>;
+
+    return (
+      <div className={`${styles['revision-history-main']} revision-history-main d-flex`}>
+        <div className="picture-container">
+          { pic }
+        </div>
+        <div className="ml-2">
+          <div className="revision-history-author mb-1">
+            <strong><Username user={author}></Username></strong>
+            { isLatestRevision && <span className="badge badge-info ml-2">Latest</span> }
+          </div>
+          <div className="mb-1">
+            <UserDate dateTime={revision.createdAt} />
+            <br className="d-xl-none d-block" />
+            <a className="ml-xl-3" href={`?revisionId=${revision._id}`}>
+              <i className="icon-login"></i> { t('Go to this version') }
+            </a>
+          </div>
+        </div>
+      </div>
+    );
+  };
+
+  if (!hasDiff) {
+    return renderSimplifiedNodiff(revision);
+  }
+
+  return renderFull(revision);
+};

+ 0 - 87
packages/app/src/components/PageHistory/RevisionDiff.jsx

@@ -1,87 +0,0 @@
-/* eslint-disable react/no-danger */
-import React from 'react';
-
-
-import { createPatch } from 'diff';
-import { html } from 'diff2html';
-import { useTranslation } from 'next-i18next';
-import PropTypes from 'prop-types';
-
-import UserDate from '../User/UserDate';
-
-class RevisionDiff extends React.Component {
-
-  render() {
-    const { t } = this.props;
-    const currentRevision = this.props.currentRevision;
-    const previousRevision = this.props.previousRevision;
-    const revisionDiffOpened = this.props.revisionDiffOpened;
-
-
-    let diffViewHTML = '';
-    if (currentRevision.body
-      && previousRevision.body
-      && revisionDiffOpened) {
-
-      let previousText = previousRevision.body;
-      // comparing ObjectId
-      // eslint-disable-next-line eqeqeq
-      if (currentRevision._id == previousRevision._id) {
-        previousText = '';
-      }
-
-      const patch = createPatch(
-        currentRevision.pageId, // currentRevision.path is DEPRECATED
-        previousText,
-        currentRevision.body,
-      );
-      const option = {
-        drawFileList: false,
-        outputFormat: 'side-by-side',
-      };
-
-      diffViewHTML = html(patch, option);
-    }
-
-    const diffView = { __html: diffViewHTML };
-    return (
-      <>
-        <div className="comparison-header">
-          <div className="container pt-1 pr-0">
-            <div className="row">
-              <div className="col comparison-source-wrapper pt-1 px-0">
-                <span className="comparison-source pr-3">{t('page_history.comparing_source')}</span><UserDate dateTime={previousRevision.createdAt} />
-                <a href={`?revisionId=${previousRevision._id}`} className="ml-3">
-                  <i className="icon-login"></i>
-                </a>
-
-              </div>
-              <div className="col comparison-target-wrapper pt-1">
-                <span className="comparison-target pr-3">{t('page_history.comparing_target')}</span><UserDate dateTime={currentRevision.createdAt} />
-                <a href={`?revisionId=${currentRevision._id}`} className="ml-3">
-                  <i className="icon-login"></i>
-                </a>
-              </div>
-            </div>
-          </div>
-        </div>
-        <div className="revision-history-diff pb-1" dangerouslySetInnerHTML={diffView} />
-      </>
-    );
-  }
-
-}
-
-RevisionDiff.propTypes = {
-  t: PropTypes.func.isRequired,
-  currentRevision: PropTypes.object.isRequired,
-  previousRevision: PropTypes.object.isRequired,
-  revisionDiffOpened: PropTypes.bool.isRequired,
-};
-
-const RevisionDiffWrapperFC = (props) => {
-  const { t } = useTranslation();
-  return <RevisionDiff t={t} {...props} />;
-};
-
-export default RevisionDiffWrapperFC;

+ 35 - 0
packages/app/src/components/PageHistory/RevisionDiff.module.scss

@@ -0,0 +1,35 @@
+@use '~/styles/bootstrap/init' as bs;
+
+.revision-diff-container :global {
+  .comparison-header {
+    height: 34px;
+    background-color: #ffffff;
+    border: 1px solid bs.$gray-300;
+    .comparison-source-wrapper {
+      height: 26px;
+      margin-right: 1px;
+      border-right: 1px solid bs.$gray-300;
+      .comparison-source {
+        color: bs.$gray-500;
+      }
+    }
+    .comparison-target-wrapper {
+      height: 26px;
+      .comparison-target {
+        color: bs.$gray-500;
+      }
+    }
+  }
+
+  .revision-history-diff {
+    color: bs.$gray-900;
+    table-layout: fixed;
+
+    // revision-history
+    // to stay d2h-code-side-line-number in the revision history diff area
+    .d2h-wrapper {
+      position: relative;
+    }
+  }
+}
+

+ 67 - 0
packages/app/src/components/PageHistory/RevisionDiff.tsx

@@ -0,0 +1,67 @@
+import React from 'react';
+
+import { IRevisionHasPageId } from '@growi/core';
+import { createPatch } from 'diff';
+import { html, Diff2HtmlConfig } from 'diff2html';
+import { useTranslation } from 'next-i18next';
+
+import UserDate from '../User/UserDate';
+
+import styles from './RevisionDiff.module.scss';
+
+import 'diff2html/bundles/css/diff2html.min.css';
+
+type RevisioinDiffProps = {
+  currentRevision: IRevisionHasPageId,
+  previousRevision: IRevisionHasPageId,
+  revisionDiffOpened: boolean,
+}
+
+export const RevisionDiff = (props: RevisioinDiffProps): JSX.Element => {
+  const { t } = useTranslation();
+
+  const { currentRevision, previousRevision, revisionDiffOpened } = props;
+
+  const previousText = (currentRevision._id === previousRevision._id) ? '' : previousRevision.body;
+
+  const patch = createPatch(
+    currentRevision.pageId, // currentRevision.path is DEPRECATED
+    previousText,
+    currentRevision.body,
+  );
+
+  const option: Diff2HtmlConfig = {
+    outputFormat: 'side-by-side',
+    drawFileList: false,
+  };
+
+  const diffViewHTML = (currentRevision.body && previousRevision.body && revisionDiffOpened) ? html(patch, option) : '';
+
+  const diffView = { __html: diffViewHTML };
+
+  return (
+    <div className={`${styles['revision-diff-container']}`}>
+      <div className='comparison-header'>
+        <div className="container pt-1 pr-0">
+          <div className="row">
+            <div className="col comparison-source-wrapper pt-1 px-0">
+              <span className="comparison-source pr-3">{t('page_history.comparing_source')}</span><UserDate dateTime={previousRevision.createdAt} />
+              <a href={`?revisionId=${previousRevision._id}`} className="ml-3">
+                <i className="icon-login"></i>
+              </a>
+
+            </div>
+            <div className="col comparison-target-wrapper pt-1">
+              <span className="comparison-target pr-3">{t('page_history.comparing_target')}</span><UserDate dateTime={currentRevision.createdAt} />
+              <a href={`?revisionId=${currentRevision._id}`} className="ml-3">
+                <i className="icon-login"></i>
+              </a>
+            </div>
+          </div>
+        </div>
+      </div>
+      <div className="revision-history-diff pb-1" dangerouslySetInnerHTML={diffView} />
+    </div>
+  );
+
+};

+ 1 - 0
packages/app/src/components/PrivateLegacyPages.tsx

@@ -455,6 +455,7 @@ const PrivateLegacyPages = (): JSX.Element => {
             toastSuccess(t('private_legacy_pages.by_path_modal.success'));
             toastSuccess(t('private_legacy_pages.by_path_modal.success'));
             setOpenConvertModal(false);
             setOpenConvertModal(false);
             mutate();
             mutate();
+            advancePt();
           }
           }
           catch (errs) {
           catch (errs) {
             if (errs.length === 1) {
             if (errs.length === 1) {

+ 14 - 0
packages/app/src/components/RevisionComparer/RevisionComparer.module.scss

@@ -0,0 +1,14 @@
+.revision-compare :global {
+  .revision-compare-container {
+    min-height: 100px;
+
+    &.nodiff {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+    }
+  }
+  .d2h-file-header {
+    display: none;
+  }
+}

+ 21 - 33
packages/app/src/components/RevisionComparer/RevisionComparer.jsx → packages/app/src/components/RevisionComparer/RevisionComparer.tsx

@@ -1,8 +1,7 @@
-import React, { useState, useEffect } from 'react';
+import React, { useState } from 'react';
 
 
-import { pagePathUtils } from '@growi/core';
+import { IRevisionHasPageId, pagePathUtils } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
-import PropTypes from 'prop-types';
 import { CopyToClipboard } from 'react-copy-to-clipboard';
 import { CopyToClipboard } from 'react-copy-to-clipboard';
 import {
 import {
   Dropdown, DropdownToggle, DropdownMenu, DropdownItem,
   Dropdown, DropdownToggle, DropdownMenu, DropdownItem,
@@ -10,36 +9,40 @@ import {
 
 
 import { useCurrentPagePath } from '~/stores/context';
 import { useCurrentPagePath } from '~/stores/context';
 
 
-import RevisionDiff from '../PageHistory/RevisionDiff';
+import { RevisionDiff } from '../PageHistory/RevisionDiff';
 
 
+import styles from './RevisionComparer.module.scss';
 
 
 const { encodeSpaces } = pagePathUtils;
 const { encodeSpaces } = pagePathUtils;
 
 
-/* eslint-disable react/prop-types */
 const DropdownItemContents = ({ title, contents }) => (
 const DropdownItemContents = ({ title, contents }) => (
   <>
   <>
     <div className="h6 mt-1 mb-2"><strong>{title}</strong></div>
     <div className="h6 mt-1 mb-2"><strong>{title}</strong></div>
     <div className="card well mb-1 p-2">{contents}</div>
     <div className="card well mb-1 p-2">{contents}</div>
   </>
   </>
 );
 );
-/* eslint-enable react/prop-types */
 
 
+type RevisionComparerProps = {
+  sourceRevision: IRevisionHasPageId
+  targetRevision: IRevisionHasPageId
+  currentPageId?: string
+}
 
 
-const RevisionComparer = (props) => {
-
+export const RevisionComparer = (props: RevisionComparerProps): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
-  const { data: currentPagePath } = useCurrentPagePath();
-  const [dropdownOpen, setDropdownOpen] = useState(false);
+
   const {
   const {
-    sourceRevision, targetRevision,
-    currentPageId,
+    sourceRevision, targetRevision, currentPageId,
   } = props;
   } = props;
 
 
-  function toggleDropdown() {
+  const { data: currentPagePath } = useCurrentPagePath();
+  const [dropdownOpen, setDropdownOpen] = useState(false);
+
+  const toggleDropdown = () => {
     setDropdownOpen(!dropdownOpen);
     setDropdownOpen(!dropdownOpen);
-  }
+  };
 
 
-  const generateURL = (pathName) => {
+  const generateURL = (pathName: string) => {
     const { origin } = window.location;
     const { origin } = window.location;
 
 
     const url = new URL(pathName, origin);
     const url = new URL(pathName, origin);
@@ -49,24 +52,17 @@ const RevisionComparer = (props) => {
       url.searchParams.set('compare', urlParams);
       url.searchParams.set('compare', urlParams);
     }
     }
 
 
-    return encodeSpaces(decodeURI(url));
-
+    return encodeSpaces(decodeURI(url.href));
   };
   };
 
 
-  let isNodiff;
-  if (sourceRevision == null || targetRevision == null) {
-    isNodiff = true;
-  }
-  else {
-    isNodiff = sourceRevision._id === targetRevision._id;
-  }
+  const isNodiff = (sourceRevision == null || targetRevision == null) ? true : sourceRevision._id === targetRevision._id;
 
 
   if (currentPageId == null || currentPagePath == null) {
   if (currentPageId == null || currentPagePath == null) {
     return <>{ t('not_found_page.page_not_exist')}</>;
     return <>{ t('not_found_page.page_not_exist')}</>;
   }
   }
 
 
   return (
   return (
-    <div className="revision-compare">
+    <div className={`${styles['revision-compare']} revision-compare`}>
       <div className="d-flex">
       <div className="d-flex">
         <h4 className="align-self-center">{ t('page_history.comparing_revisions') }</h4>
         <h4 className="align-self-center">{ t('page_history.comparing_revisions') }</h4>
         <Dropdown
         <Dropdown
@@ -115,11 +111,3 @@ const RevisionComparer = (props) => {
     </div>
     </div>
   );
   );
 };
 };
-
-RevisionComparer.propTypes = {
-  sourceRevision: PropTypes.instanceOf(Object),
-  targetRevision: PropTypes.instanceOf(Object),
-  currentPageId: PropTypes.string,
-};
-
-export default RevisionComparer;

+ 3 - 3
packages/app/src/components/SearchPage.tsx

@@ -9,7 +9,7 @@ import { useRouter } from 'next/router';
 
 
 import { ISelectableAll, ISelectableAndIndeterminatable } from '~/client/interfaces/selectable-all';
 import { ISelectableAll, ISelectableAndIndeterminatable } from '~/client/interfaces/selectable-all';
 import { IFormattedSearchResult } from '~/interfaces/search';
 import { IFormattedSearchResult } from '~/interfaces/search';
-import { useIsSearchServiceReachable } from '~/stores/context';
+import { useIsSearchServiceReachable, useShowPageLimitationL } from '~/stores/context';
 import { ISearchConditions, ISearchConfigurations, useSWRxSearch } from '~/stores/search';
 import { ISearchConditions, ISearchConfigurations, useSWRxSearch } from '~/stores/search';
 
 
 import PaginationWrapper from './PaginationWrapper';
 import PaginationWrapper from './PaginationWrapper';
@@ -89,6 +89,7 @@ SearchResultListHead.displayName = 'SearchResultListHead';
 
 
 export const SearchPage = (): JSX.Element => {
 export const SearchPage = (): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
+  const { data: showPageLimitationL } = useShowPageLimitationL();
   const router = useRouter();
   const router = useRouter();
 
 
   // parse URL Query
   // parse URL Query
@@ -97,9 +98,8 @@ export const SearchPage = (): JSX.Element => {
 
 
   const [keyword, setKeyword] = useState<string>(initQ);
   const [keyword, setKeyword] = useState<string>(initQ);
   const [offset, setOffset] = useState<number>(0);
   const [offset, setOffset] = useState<number>(0);
-  const [limit, setLimit] = useState<number>(INITIAL_PAGIONG_SIZE);
+  const [limit, setLimit] = useState<number>(showPageLimitationL ?? INITIAL_PAGIONG_SIZE);
   const [configurationsByControl, setConfigurationsByControl] = useState<Partial<ISearchConfigurations>>({});
   const [configurationsByControl, setConfigurationsByControl] = useState<Partial<ISearchConfigurations>>({});
-
   const selectAllControlRef = useRef<ISelectableAndIndeterminatable|null>(null);
   const selectAllControlRef = useRef<ISelectableAndIndeterminatable|null>(null);
   const searchPageBaseRef = useRef<ISelectableAll & IReturnSelectedPageIds|null>(null);
   const searchPageBaseRef = useRef<ISelectableAll & IReturnSelectedPageIds|null>(null);
 
 

+ 0 - 0
packages/app/src/pages/UnsavedAlertDialog.tsx → packages/app/src/components/UnsavedAlertDialog.tsx


+ 35 - 0
packages/app/src/migrations/20221014130200-remove-customize-is-saved-states-of-tab-changes.js

@@ -0,0 +1,35 @@
+// eslint-disable-next-line import/no-named-as-default
+import Config from '~/server/models/config';
+import { getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
+import loggerFactory from '~/utils/logger';
+
+
+const logger = loggerFactory('growi:migrate:remove-isSavedStatesOfTabChanges');
+
+const mongoose = require('mongoose');
+
+module.exports = {
+  async up() {
+    logger.info('Apply migration');
+    mongoose.connect(getMongoUri(), mongoOptions);
+
+    await Config.findOneAndDelete({ key: 'customize:isSavedStatesOfTabChanges' }); // remove isSavedStatesOfTabChanges
+
+    logger.info('Migration has successfully applied');
+  },
+
+  async down() {
+    logger.info('Rollback migration');
+    mongoose.connect(getMongoUri(), mongoOptions);
+
+    const insertConfig = new Config({
+      ns: 'crowi',
+      key: 'customize:isSavedStatesOfTabChanges',
+      value: false,
+    });
+
+    await insertConfig.save();
+
+    logger.info('Migration has been successfully rollbacked');
+  },
+};

+ 28 - 21
packages/app/src/pages/[[...path]].page.tsx

@@ -4,10 +4,12 @@ import React, { useEffect } from 'react';
 import EventEmitter from 'events';
 import EventEmitter from 'events';
 
 
 import {
 import {
-  IDataWithMeta, IPageInfoForEntity, IPagePopulatedToShowRevision, isClient, isIPageInfoForEntity, IUser, IUserHasId, pagePathUtils, pathUtils,
+  isClient, isIPageInfoForEntity, pagePathUtils, pathUtils,
+} from '@growi/core';
+import type {
+  IDataWithMeta, IPageInfoForEntity, IPagePopulatedToShowRevision, IUser, IUserHasId,
 } from '@growi/core';
 } from '@growi/core';
 import ExtensibleCustomError from 'extensible-custom-error';
 import ExtensibleCustomError from 'extensible-custom-error';
-import { model as mongooseModel } from 'mongoose';
 import {
 import {
   NextPage, GetServerSideProps, GetServerSidePropsContext,
   NextPage, GetServerSideProps, GetServerSidePropsContext,
 } from 'next';
 } from 'next';
@@ -22,20 +24,18 @@ import { PageAlerts } from '~/components/PageAlert/PageAlerts';
 // import { useTranslation } from '~/i18n';
 // import { useTranslation } from '~/i18n';
 import { CurrentPageContentFooter } from '~/components/PageContentFooter';
 import { CurrentPageContentFooter } from '~/components/PageContentFooter';
 import { UsersHomePageFooterProps } from '~/components/UsersHomePageFooter';
 import { UsersHomePageFooterProps } from '~/components/UsersHomePageFooter';
-import { CrowiRequest } from '~/interfaces/crowi-request';
+import type { CrowiRequest } from '~/interfaces/crowi-request';
 // import { renderScriptTagByName, renderHighlightJsStyleTag } from '~/service/cdn-resources-loader';
 // import { renderScriptTagByName, renderHighlightJsStyleTag } from '~/service/cdn-resources-loader';
-// import { useIndentSize } from '~/stores/editor';
 // import { useRendererSettings } from '~/stores/renderer';
 // import { useRendererSettings } from '~/stores/renderer';
 // import { EditorMode, useEditorMode, useIsMobile } from '~/stores/ui';
 // import { EditorMode, useEditorMode, useIsMobile } from '~/stores/ui';
-import { EditorConfig } from '~/interfaces/editor-settings';
-import { CustomWindow } from '~/interfaces/global';
-import { RendererConfig } from '~/interfaces/services/renderer';
-import { ISidebarConfig } from '~/interfaces/sidebar-config';
-import { IUserUISettings } from '~/interfaces/user-ui-settings';
-import { PageModel, PageDocument } from '~/server/models/page';
-import { PageRedirectModel } from '~/server/models/page-redirect';
-import { UserUISettingsModel } from '~/server/models/user-ui-settings';
-import { useSWRxLayoutSetting } from '~/stores/admin/customize';
+import type { EditorConfig } from '~/interfaces/editor-settings';
+import type { CustomWindow } from '~/interfaces/global';
+import type { RendererConfig } from '~/interfaces/services/renderer';
+import type { ISidebarConfig } from '~/interfaces/sidebar-config';
+import type { IUserUISettings } from '~/interfaces/user-ui-settings';
+import type { PageModel, PageDocument } from '~/server/models/page';
+import type { PageRedirectModel } from '~/server/models/page-redirect';
+import type { UserUISettingsModel } from '~/server/models/user-ui-settings';
 import { useSWRxCurrentPage, useSWRxIsGrantNormalized, useSWRxPageInfo } from '~/stores/page';
 import { useSWRxCurrentPage, useSWRxIsGrantNormalized, useSWRxPageInfo } from '~/stores/page';
 import { useRedirectFrom } from '~/stores/page-redirect';
 import { useRedirectFrom } from '~/stores/page-redirect';
 import {
 import {
@@ -59,7 +59,7 @@ import {
   useIsForbidden, useIsNotFound, useIsTrashPage, useIsSharedUser,
   useIsForbidden, useIsNotFound, useIsTrashPage, useIsSharedUser,
   useIsEnabledStaleNotification, useIsIdenticalPath,
   useIsEnabledStaleNotification, useIsIdenticalPath,
   useIsSearchServiceConfigured, useIsSearchServiceReachable, useDisableLinkSharing,
   useIsSearchServiceConfigured, useIsSearchServiceReachable, useDisableLinkSharing,
-  useDrawioUri, useHackmdUri,
+  useDrawioUri, useHackmdUri, useDefaultIndentSize, useIsIndentSizeForced,
   useIsAclEnabled, useIsUserPage, useIsSearchPage,
   useIsAclEnabled, useIsUserPage, useIsSearchPage,
   useCsrfToken, useIsSearchScopeChildrenAsDefault, useCurrentPageId, useCurrentPathname,
   useCsrfToken, useIsSearchScopeChildrenAsDefault, useCurrentPageId, useCurrentPathname,
   useIsSlackConfigured, useRendererConfig, useEditingMarkdown,
   useIsSlackConfigured, useRendererConfig, useEditingMarkdown,
@@ -75,7 +75,7 @@ import { calcIsContainerFluid } from './utils/layout';
 
 
 const NotCreatablePage = dynamic(() => import('../components/NotCreatablePage').then(mod => mod.NotCreatablePage), { ssr: false });
 const NotCreatablePage = dynamic(() => import('../components/NotCreatablePage').then(mod => mod.NotCreatablePage), { ssr: false });
 const ForbiddenPage = dynamic(() => import('../components/ForbiddenPage'), { ssr: false });
 const ForbiddenPage = dynamic(() => import('../components/ForbiddenPage'), { ssr: false });
-const UnsavedAlertDialog = dynamic(() => import('./UnsavedAlertDialog'), { ssr: false });
+const UnsavedAlertDialog = dynamic(() => import('../components/UnsavedAlertDialog'), { ssr: false });
 const GrowiSubNavigationSwitcher = dynamic(() => import('../components/Navbar/GrowiSubNavigationSwitcher'), { ssr: false });
 const GrowiSubNavigationSwitcher = dynamic(() => import('../components/Navbar/GrowiSubNavigationSwitcher'), { ssr: false });
 const UsersHomePageFooter = dynamic<UsersHomePageFooterProps>(() => import('../components/UsersHomePageFooter')
 const UsersHomePageFooter = dynamic<UsersHomePageFooterProps>(() => import('../components/UsersHomePageFooter')
   .then(mod => mod.UsersHomePageFooter), { ssr: false });
   .then(mod => mod.UsersHomePageFooter), { ssr: false });
@@ -159,8 +159,8 @@ type Props = CommonProps & {
   isEnabledStaleNotification: boolean,
   isEnabledStaleNotification: boolean,
   // isEnabledLinebreaks: boolean,
   // isEnabledLinebreaks: boolean,
   // isEnabledLinebreaksInComments: boolean,
   // isEnabledLinebreaksInComments: boolean,
-  // adminPreferredIndentSize: number,
-  // isIndentSizeForced: boolean,
+  adminPreferredIndentSize: number,
+  isIndentSizeForced: boolean,
   disableLinkSharing: boolean,
   disableLinkSharing: boolean,
 
 
   rendererConfig: RendererConfig,
   rendererConfig: RendererConfig,
@@ -219,7 +219,8 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
   useDrawioUri(props.drawioUri);
   useDrawioUri(props.drawioUri);
   useHackmdUri(props.hackmdUri);
   useHackmdUri(props.hackmdUri);
   // useNoCdn(props.noCdn);
   // useNoCdn(props.noCdn);
-  // useIndentSize(props.adminPreferredIndentSize);
+  useDefaultIndentSize(props.adminPreferredIndentSize);
+  useIsIndentSizeForced(props.isIndentSizeForced);
   useDisableLinkSharing(props.disableLinkSharing);
   useDisableLinkSharing(props.disableLinkSharing);
   useRendererConfig(props.rendererConfig);
   useRendererConfig(props.rendererConfig);
   // useRendererSettings(props.rendererSettingsStr != null ? JSON.parse(props.rendererSettingsStr) : undefined);
   // useRendererSettings(props.rendererSettingsStr != null ? JSON.parse(props.rendererSettingsStr) : undefined);
@@ -304,7 +305,9 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
       <BasicLayout title={useCustomTitle(props, 'GROWI')} className={classNames.join(' ')} expandContainer={isContainerFluid}>
       <BasicLayout title={useCustomTitle(props, 'GROWI')} className={classNames.join(' ')} expandContainer={isContainerFluid}>
         <div className="h-100 d-flex flex-column justify-content-between">
         <div className="h-100 d-flex flex-column justify-content-between">
           <header className="py-0 position-relative">
           <header className="py-0 position-relative">
-            <GrowiContextualSubNavigation isLinkSharingDisabled={props.disableLinkSharing} />
+            <div id="grw-subnav-container">
+              <GrowiContextualSubNavigation isLinkSharingDisabled={props.disableLinkSharing} />
+            </div>
           </header>
           </header>
           <div className="d-edit-none">
           <div className="d-edit-none">
             <GrowiSubNavigationSwitcher />
             <GrowiSubNavigationSwitcher />
@@ -373,6 +376,8 @@ class MultiplePagesHitsError extends ExtensibleCustomError {
 }
 }
 
 
 async function injectPageData(context: GetServerSidePropsContext, props: Props): Promise<void> {
 async function injectPageData(context: GetServerSidePropsContext, props: Props): Promise<void> {
+  const { model: mongooseModel } = await import('mongoose');
+
   const req: CrowiRequest = context.req as CrowiRequest;
   const req: CrowiRequest = context.req as CrowiRequest;
   const { crowi } = req;
   const { crowi } = req;
   const { revisionId } = req.query;
   const { revisionId } = req.query;
@@ -425,6 +430,8 @@ async function injectPageData(context: GetServerSidePropsContext, props: Props):
 }
 }
 
 
 async function injectUserUISettings(context: GetServerSidePropsContext, props: Props): Promise<void> {
 async function injectUserUISettings(context: GetServerSidePropsContext, props: Props): Promise<void> {
+  const { model: mongooseModel } = await import('mongoose');
+
   const req = context.req as CrowiRequest<IUserHasId & any>;
   const req = context.req as CrowiRequest<IUserHasId & any>;
   const { user } = req;
   const { user } = req;
   const UserUISettings = mongooseModel('UserUISettings') as UserUISettingsModel;
   const UserUISettings = mongooseModel('UserUISettings') as UserUISettingsModel;
@@ -519,8 +526,8 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
       isUploadableImage: crowi.fileUploadService.getIsUploadable(),
       isUploadableImage: crowi.fileUploadService.getIsUploadable(),
     },
     },
   };
   };
-  // props.adminPreferredIndentSize = configManager.getConfig('markdown', 'markdown:adminPreferredIndentSize');
-  // props.isIndentSizeForced = configManager.getConfig('markdown', 'markdown:isIndentSizeForced');
+  props.adminPreferredIndentSize = configManager.getConfig('markdown', 'markdown:adminPreferredIndentSize');
+  props.isIndentSizeForced = configManager.getConfig('markdown', 'markdown:isIndentSizeForced');
 
 
   props.rendererConfig = {
   props.rendererConfig = {
     isEnabledLinebreaks: configManager.getConfig('markdown', 'markdown:isEnabledLinebreaks'),
     isEnabledLinebreaks: configManager.getConfig('markdown', 'markdown:isEnabledLinebreaks'),

+ 1 - 8
packages/app/src/pages/_app.page.tsx

@@ -3,8 +3,6 @@ import React, { useEffect } from 'react';
 import { isServer } from '@growi/core';
 import { isServer } from '@growi/core';
 import { appWithTranslation } from 'next-i18next';
 import { appWithTranslation } from 'next-i18next';
 import { AppProps } from 'next/app';
 import { AppProps } from 'next/app';
-import { DndProvider } from 'react-dnd';
-import { HTML5Backend } from 'react-dnd-html5-backend';
 import { SWRConfig } from 'swr';
 import { SWRConfig } from 'swr';
 
 
 import * as nextI18nConfig from '^/config/next-i18next.config';
 import * as nextI18nConfig from '^/config/next-i18next.config';
@@ -13,7 +11,6 @@ import { useI18nextHMR } from '~/services/i18next-hmr';
 import {
 import {
   useAppTitle, useConfidential, useGrowiTheme, useGrowiVersion, useSiteUrl,
   useAppTitle, useConfidential, useGrowiTheme, useGrowiVersion, useSiteUrl,
 } from '~/stores/context';
 } from '~/stores/context';
-import { NextThemesProvider } from '~/stores/use-next-themes';
 import { SWRConfigValue, swrGlobalConfiguration } from '~/utils/swr-utils';
 import { SWRConfigValue, swrGlobalConfiguration } from '~/utils/swr-utils';
 
 
 
 
@@ -59,11 +56,7 @@ function GrowiApp({ Component, pageProps }: GrowiAppProps): JSX.Element {
 
 
   return (
   return (
     <SWRConfig value={swrConfig}>
     <SWRConfig value={swrConfig}>
-      <NextThemesProvider>
-        <DndProvider backend={HTML5Backend}>
-          <Component {...pageProps} />
-        </DndProvider>
-      </NextThemesProvider>
+      <Component {...pageProps} />
     </SWRConfig>
     </SWRConfig>
   );
   );
 }
 }

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

@@ -5,13 +5,12 @@ import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 import Head from 'next/head';
 import Head from 'next/head';
 
 
-import { BasicLayout } from '~/components/Layout/BasicLayout';
-import { CrowiRequest } from '~/interfaces/crowi-request';
-import { RendererConfig } from '~/interfaces/services/renderer';
-import { ISidebarConfig } from '~/interfaces/sidebar-config';
-import { IUser, IUserHasId } from '~/interfaces/user';
-import { IUserUISettings } from '~/interfaces/user-ui-settings';
-import UserUISettings from '~/server/models/user-ui-settings';
+import type { CrowiRequest } from '~/interfaces/crowi-request';
+import type { RendererConfig } from '~/interfaces/services/renderer';
+import type { ISidebarConfig } from '~/interfaces/sidebar-config';
+import type { IUser, IUserHasId } from '~/interfaces/user';
+import type { IUserUISettings } from '~/interfaces/user-ui-settings';
+import type { UserUISettingsModel } from '~/server/models/user-ui-settings';
 import {
 import {
   useCsrfToken, useCurrentUser, useIsSearchPage, useIsSearchScopeChildrenAsDefault,
   useCsrfToken, useCurrentUser, useIsSearchPage, useIsSearchScopeChildrenAsDefault,
   useIsSearchServiceConfigured, useIsSearchServiceReachable, useRendererConfig,
   useIsSearchServiceConfigured, useIsSearchServiceReachable, useRendererConfig,
@@ -89,9 +88,12 @@ const PrivateLegacyPage: NextPage<Props> = (props: Props) => {
 };
 };
 
 
 async function injectUserUISettings(context: GetServerSidePropsContext, props: Props): Promise<void> {
 async function injectUserUISettings(context: GetServerSidePropsContext, props: Props): Promise<void> {
+  const { model: mongooseModel } = await import('mongoose');
+
   const req = context.req as CrowiRequest<IUserHasId & any>;
   const req = context.req as CrowiRequest<IUserHasId & any>;
   const { user } = req;
   const { user } = req;
 
 
+  const UserUISettings = mongooseModel('UserUISettings') as UserUISettingsModel;
   const userUISettings = user == null ? null : await UserUISettings.findOne({ user: user._id }).exec();
   const userUISettings = user == null ? null : await UserUISettings.findOne({ user: user._id }).exec();
   if (userUISettings != null) {
   if (userUISettings != null) {
     props.userUISettings = userUISettings.toObject();
     props.userUISettings = userUISettings.toObject();

+ 17 - 9
packages/app/src/pages/_search.page.tsx

@@ -5,17 +5,15 @@ import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 import Head from 'next/head';
 import Head from 'next/head';
 
 
-import { BasicLayout } from '~/components/Layout/BasicLayout';
-import { CrowiRequest } from '~/interfaces/crowi-request';
-import { RendererConfig } from '~/interfaces/services/renderer';
-import { ISidebarConfig } from '~/interfaces/sidebar-config';
-import { IUser, IUserHasId } from '~/interfaces/user';
-import { IUserUISettings } from '~/interfaces/user-ui-settings';
-import UserUISettings from '~/server/models/user-ui-settings';
-import Xss from '~/services/xss';
+import type { CrowiRequest } from '~/interfaces/crowi-request';
+import type { RendererConfig } from '~/interfaces/services/renderer';
+import type { ISidebarConfig } from '~/interfaces/sidebar-config';
+import type { IUser, IUserHasId } from '~/interfaces/user';
+import type { IUserUISettings } from '~/interfaces/user-ui-settings';
+import type { UserUISettingsModel } from '~/server/models/user-ui-settings';
 import {
 import {
   useCsrfToken, useCurrentUser, useIsSearchPage, useIsSearchScopeChildrenAsDefault,
   useCsrfToken, useCurrentUser, useIsSearchPage, useIsSearchScopeChildrenAsDefault,
-  useIsSearchServiceConfigured, useIsSearchServiceReachable, useRendererConfig,
+  useIsSearchServiceConfigured, useIsSearchServiceReachable, useRendererConfig, useShowPageLimitationL,
 } from '~/stores/context';
 } from '~/stores/context';
 import {
 import {
   usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser, useSidebarCollapsed,
   usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser, useSidebarCollapsed,
@@ -45,6 +43,9 @@ type Props = CommonProps & {
   // Render config
   // Render config
   rendererConfig: RendererConfig,
   rendererConfig: RendererConfig,
 
 
+  // search limit
+  showPageLimitationL: number
+
 };
 };
 
 
 const SearchResultPage: NextPage<Props> = (props: Props) => {
 const SearchResultPage: NextPage<Props> = (props: Props) => {
@@ -71,6 +72,8 @@ const SearchResultPage: NextPage<Props> = (props: Props) => {
   // render config
   // render config
   useRendererConfig(props.rendererConfig);
   useRendererConfig(props.rendererConfig);
 
 
+  useShowPageLimitationL(props.showPageLimitationL);
+
   const PutbackPageModal = (): JSX.Element => {
   const PutbackPageModal = (): JSX.Element => {
     const PutbackPageModal = dynamic(() => import('../components/PutbackPageModal'), { ssr: false });
     const PutbackPageModal = dynamic(() => import('../components/PutbackPageModal'), { ssr: false });
     return <PutbackPageModal />;
     return <PutbackPageModal />;
@@ -102,9 +105,12 @@ const SearchResultPage: NextPage<Props> = (props: Props) => {
 };
 };
 
 
 async function injectUserUISettings(context: GetServerSidePropsContext, props: Props): Promise<void> {
 async function injectUserUISettings(context: GetServerSidePropsContext, props: Props): Promise<void> {
+  const { model: mongooseModel } = await import('mongoose');
+
   const req = context.req as CrowiRequest<IUserHasId & any>;
   const req = context.req as CrowiRequest<IUserHasId & any>;
   const { user } = req;
   const { user } = req;
 
 
+  const UserUISettings = mongooseModel('UserUISettings') as UserUISettingsModel;
   const userUISettings = user == null ? null : await UserUISettings.findOne({ user: user._id }).exec();
   const userUISettings = user == null ? null : await UserUISettings.findOne({ user: user._id }).exec();
   if (userUISettings != null) {
   if (userUISettings != null) {
     props.userUISettings = userUISettings.toObject();
     props.userUISettings = userUISettings.toObject();
@@ -140,6 +146,8 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
     tagWhiteList: crowi.xssService.getTagWhiteList(),
     tagWhiteList: crowi.xssService.getTagWhiteList(),
     highlightJsStyleBorder: crowi.configManager.getConfig('crowi', 'customize:highlightJsStyleBorder'),
     highlightJsStyleBorder: crowi.configManager.getConfig('crowi', 'customize:highlightJsStyleBorder'),
   };
   };
+
+  props.showPageLimitationL = configManager.getConfig('crowi', 'customize:showPageLimitationL');
 }
 }
 
 
 /**
 /**

+ 2 - 2
packages/app/src/pages/invited.page.tsx

@@ -1,13 +1,13 @@
 import React from 'react';
 import React from 'react';
 
 
-import { IUserHasId, IUser } from '@growi/core';
+import type { IUserHasId, IUser } from '@growi/core';
 import { NextPage, GetServerSideProps, GetServerSidePropsContext } from 'next';
 import { NextPage, GetServerSideProps, GetServerSidePropsContext } from 'next';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 
 
 import { InvitedFormProps } from '~/components/InvitedForm';
 import { InvitedFormProps } from '~/components/InvitedForm';
 import { NoLoginLayout } from '~/components/Layout/NoLoginLayout';
 import { NoLoginLayout } from '~/components/Layout/NoLoginLayout';
-import { CrowiRequest } from '~/interfaces/crowi-request';
+import type { CrowiRequest } from '~/interfaces/crowi-request';
 
 
 import { useCsrfToken, useCurrentPathname, useCurrentUser } from '../stores/context';
 import { useCsrfToken, useCurrentPathname, useCurrentUser } from '../stores/context';
 
 

+ 1 - 1
packages/app/src/pages/login.page.tsx

@@ -8,7 +8,7 @@ import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 
 
 import { NoLoginLayout } from '~/components/Layout/NoLoginLayout';
 import { NoLoginLayout } from '~/components/Layout/NoLoginLayout';
 import { LoginForm } from '~/components/LoginForm';
 import { LoginForm } from '~/components/LoginForm';
-import { CrowiRequest } from '~/interfaces/crowi-request';
+import type { CrowiRequest } from '~/interfaces/crowi-request';
 
 
 import {
 import {
   useCsrfToken,
   useCsrfToken,

+ 2 - 4
packages/app/src/pages/maintenance.page.tsx

@@ -1,13 +1,11 @@
-import {
-  IUser, IUserHasId,
-} from '@growi/core';
+import type { IUser, IUserHasId } from '@growi/core';
 import { NextPage, GetServerSideProps, GetServerSidePropsContext } from 'next';
 import { NextPage, GetServerSideProps, GetServerSidePropsContext } from 'next';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 
 
 import { toastError } from '~/client/util/apiNotification';
 import { toastError } from '~/client/util/apiNotification';
 import { apiv3Post } from '~/client/util/apiv3-client';
 import { apiv3Post } from '~/client/util/apiv3-client';
-import { CrowiRequest } from '~/interfaces/crowi-request';
+import type { CrowiRequest } from '~/interfaces/crowi-request';
 import { useCurrentUser } from '~/stores/context';
 import { useCurrentUser } from '~/stores/context';
 
 
 import {
 import {

+ 9 - 7
packages/app/src/pages/tags.page.tsx

@@ -1,17 +1,15 @@
 import React, { useState, useCallback } from 'react';
 import React, { useState, useCallback } from 'react';
 
 
-import {
-  IUser, IUserHasId,
-} from '@growi/core';
+import type { IUser, IUserHasId } from '@growi/core';
 import { NextPage, GetServerSideProps, GetServerSidePropsContext } from 'next';
 import { NextPage, GetServerSideProps, GetServerSidePropsContext } from 'next';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 import Head from 'next/head';
 import Head from 'next/head';
 
 
-import { CrowiRequest } from '~/interfaces/crowi-request';
-import { IDataTagCount } from '~/interfaces/tag';
-import { IUserUISettings } from '~/interfaces/user-ui-settings';
-import UserUISettings from '~/server/models/user-ui-settings';
+import type { CrowiRequest } from '~/interfaces/crowi-request';
+import type { IDataTagCount } from '~/interfaces/tag';
+import type { IUserUISettings } from '~/interfaces/user-ui-settings';
+import type { UserUISettingsModel } from '~/server/models/user-ui-settings';
 import { useSWRxTagsList } from '~/stores/tag';
 import { useSWRxTagsList } from '~/stores/tag';
 
 
 import { BasicLayout } from '../components/Layout/BasicLayout';
 import { BasicLayout } from '../components/Layout/BasicLayout';
@@ -96,8 +94,12 @@ const TagPage: NextPage<CommonProps> = (props: Props) => {
 };
 };
 
 
 async function injectUserUISettings(context: GetServerSidePropsContext, props: Props): Promise<void> {
 async function injectUserUISettings(context: GetServerSidePropsContext, props: Props): Promise<void> {
+  const { model: mongooseModel } = await import('mongoose');
+
   const req = context.req as CrowiRequest<IUserHasId & any>;
   const req = context.req as CrowiRequest<IUserHasId & any>;
   const { user } = req;
   const { user } = req;
+
+  const UserUISettings = mongooseModel('UserUISettings') as UserUISettingsModel;
   const userUISettings = user == null ? null : await UserUISettings.findOne({ user: user._id }).exec();
   const userUISettings = user == null ? null : await UserUISettings.findOne({ user: user._id }).exec();
 
 
   if (userUISettings != null) {
   if (userUISettings != null) {

+ 14 - 8
packages/app/src/pages/trash.page.tsx

@@ -1,21 +1,19 @@
 import React from 'react';
 import React from 'react';
 
 
-import {
-  IUser, IUserHasId,
-} from '@growi/core';
+import type { IUser, IUserHasId } from '@growi/core';
 import { NextPage, GetServerSideProps, GetServerSidePropsContext } from 'next';
 import { NextPage, GetServerSideProps, GetServerSidePropsContext } from 'next';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 
 
-import { CrowiRequest } from '~/interfaces/crowi-request';
-import { IUserUISettings } from '~/interfaces/user-ui-settings';
-import UserUISettings from '~/server/models/user-ui-settings';
+import type { CrowiRequest } from '~/interfaces/crowi-request';
+import type { IUserUISettings } from '~/interfaces/user-ui-settings';
+import type { UserUISettingsModel } from '~/server/models/user-ui-settings';
 
 
 import { BasicLayout } from '../components/Layout/BasicLayout';
 import { BasicLayout } from '../components/Layout/BasicLayout';
 import GrowiContextualSubNavigation from '../components/Navbar/GrowiContextualSubNavigation';
 import GrowiContextualSubNavigation from '../components/Navbar/GrowiContextualSubNavigation';
 import {
 import {
   useCurrentUser, useCurrentPageId, useCurrentPagePath, useCurrentPathname,
   useCurrentUser, useCurrentPageId, useCurrentPagePath, useCurrentPathname,
   useIsSearchServiceConfigured, useIsSearchServiceReachable,
   useIsSearchServiceConfigured, useIsSearchServiceReachable,
-  useIsSearchScopeChildrenAsDefault, useIsSearchPage,
+  useIsSearchScopeChildrenAsDefault, useIsSearchPage, useShowPageLimitationXL,
 } from '../stores/context';
 } from '../stores/context';
 
 
 import {
 import {
@@ -31,7 +29,8 @@ type Props = CommonProps & {
   isSearchServiceConfigured: boolean,
   isSearchServiceConfigured: boolean,
   isSearchServiceReachable: boolean,
   isSearchServiceReachable: boolean,
   isSearchScopeChildrenAsDefault: boolean,
   isSearchScopeChildrenAsDefault: boolean,
-  userUISettings?: IUserUISettings
+  userUISettings?: IUserUISettings,
+  showPageLimitationXL: number,
 };
 };
 
 
 const TrashPage: NextPage<CommonProps> = (props: Props) => {
 const TrashPage: NextPage<CommonProps> = (props: Props) => {
@@ -46,6 +45,8 @@ const TrashPage: NextPage<CommonProps> = (props: Props) => {
   useCurrentPathname('/trash');
   useCurrentPathname('/trash');
   useCurrentPagePath('/trash');
   useCurrentPagePath('/trash');
 
 
+  useShowPageLimitationXL(props.showPageLimitationXL);
+
   return (
   return (
     <>
     <>
       <BasicLayout title={useCustomTitle(props, 'GROWI')} >
       <BasicLayout title={useCustomTitle(props, 'GROWI')} >
@@ -67,8 +68,12 @@ const TrashPage: NextPage<CommonProps> = (props: Props) => {
 };
 };
 
 
 async function injectUserUISettings(context: GetServerSidePropsContext, props: Props): Promise<void> {
 async function injectUserUISettings(context: GetServerSidePropsContext, props: Props): Promise<void> {
+  const { model: mongooseModel } = await import('mongoose');
+
   const req = context.req as CrowiRequest<IUserHasId & any>;
   const req = context.req as CrowiRequest<IUserHasId & any>;
   const { user } = req;
   const { user } = req;
+
+  const UserUISettings = mongooseModel('UserUISettings') as UserUISettingsModel;
   const userUISettings = user == null ? null : await UserUISettings.findOne({ user: user._id }).exec();
   const userUISettings = user == null ? null : await UserUISettings.findOne({ user: user._id }).exec();
 
 
   if (userUISettings != null) {
   if (userUISettings != null) {
@@ -86,6 +91,7 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
   props.isSearchServiceConfigured = searchService.isConfigured;
   props.isSearchServiceConfigured = searchService.isConfigured;
   props.isSearchServiceReachable = searchService.isReachable;
   props.isSearchServiceReachable = searchService.isReachable;
   props.isSearchScopeChildrenAsDefault = configManager.getConfig('crowi', 'customize:isSearchScopeChildrenAsDefault');
   props.isSearchScopeChildrenAsDefault = configManager.getConfig('crowi', 'customize:isSearchScopeChildrenAsDefault');
+  props.showPageLimitationXL = crowi.configManager.getConfig('crowi', 'customize:showPageLimitationXL');
 }
 }
 
 
 export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
 export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {

+ 6 - 5
packages/app/src/server/crowi/index.js

@@ -418,20 +418,21 @@ Crowi.prototype.getTokens = function() {
 
 
 Crowi.prototype.start = async function() {
 Crowi.prototype.start = async function() {
   const dev = process.env.NODE_ENV !== 'production';
   const dev = process.env.NODE_ENV !== 'production';
-  this.nextApp = next({ dev });
 
 
+  await this.init();
+  await this.buildServer();
+
+  // setup Next.js
+  this.nextApp = next({ dev });
   await this.nextApp.prepare();
   await this.nextApp.prepare();
 
 
-  // init CrowiDev
+  // setup CrowiDev
   if (dev) {
   if (dev) {
     const CrowiDev = require('./dev');
     const CrowiDev = require('./dev');
     this.crowiDev = new CrowiDev(this);
     this.crowiDev = new CrowiDev(this);
     this.crowiDev.init();
     this.crowiDev.init();
   }
   }
 
 
-  await this.init();
-  await this.buildServer();
-
   const { express, configManager } = this;
   const { express, configManager } = this;
 
 
   const app = (this.node_env === 'development') ? this.crowiDev.setupServer(express) : express;
   const app = (this.node_env === 'development') ? this.crowiDev.setupServer(express) : express;

+ 0 - 3
packages/app/src/server/models/config.ts

@@ -38,7 +38,6 @@ export const generateConfigsForInstalling = (): { [key: string]: any } => {
   // overwrite
   // overwrite
   config['app:installed'] = true;
   config['app:installed'] = true;
   config['app:fileUpload'] = true;
   config['app:fileUpload'] = true;
-  config['customize:isSavedStatesOfTabChanges'] = false;
   config['app:isV5Compatible'] = true;
   config['app:isV5Compatible'] = true;
 
 
   return config;
   return config;
@@ -129,7 +128,6 @@ export const defaultCrowiConfigs: { [key: string]: any } = {
   'customize:theme' : GrowiThemes.DEFAULT,
   'customize:theme' : GrowiThemes.DEFAULT,
   'customize:isContainerFluid' : false,
   'customize:isContainerFluid' : false,
   'customize:isEnabledTimeline' : true,
   'customize:isEnabledTimeline' : true,
-  'customize:isSavedStatesOfTabChanges' : true,
   'customize:isEnabledAttachTitleHeader' : false,
   'customize:isEnabledAttachTitleHeader' : false,
   'customize:showPageLimitationS' : 20,
   'customize:showPageLimitationS' : 20,
   'customize:showPageLimitationM' : 10,
   'customize:showPageLimitationM' : 10,
@@ -228,7 +226,6 @@ schema.statics.getLocalconfig = function(crowi) {
     customizeTitle: crowi.configManager.getConfig('crowi', 'customize:title'),
     customizeTitle: crowi.configManager.getConfig('crowi', 'customize:title'),
     customizeHeader: crowi.configManager.getConfig('crowi', 'customize:header'),
     customizeHeader: crowi.configManager.getConfig('crowi', 'customize:header'),
     customizeCss: crowi.configManager.getConfig('crowi', 'customize:css'),
     customizeCss: crowi.configManager.getConfig('crowi', 'customize:css'),
-    isSavedStatesOfTabChanges: crowi.configManager.getConfig('crowi', 'customize:isSavedStatesOfTabChanges'),
     isEnabledAttachTitleHeader: crowi.configManager.getConfig('crowi', 'customize:isEnabledAttachTitleHeader'),
     isEnabledAttachTitleHeader: crowi.configManager.getConfig('crowi', 'customize:isEnabledAttachTitleHeader'),
     customizeScript: crowi.configManager.getConfig('crowi', 'customize:script'),
     customizeScript: crowi.configManager.getConfig('crowi', 'customize:script'),
     isSlackConfigured: crowi.slackIntegrationService.isSlackConfigured,
     isSlackConfigured: crowi.slackIntegrationService.isSlackConfigured,

+ 0 - 6
packages/app/src/server/routes/apiv3/customize-setting.js

@@ -49,8 +49,6 @@ const multer = require('multer');
  *        properties:
  *        properties:
  *          isEnabledTimeline:
  *          isEnabledTimeline:
  *            type: boolean
  *            type: boolean
- *          isSavedStatesOfTabChanges:
- *            type: boolean
  *          isEnabledAttachTitleHeader:
  *          isEnabledAttachTitleHeader:
  *            type: boolean
  *            type: boolean
  *          pageLimitationS:
  *          pageLimitationS:
@@ -122,7 +120,6 @@ module.exports = (crowi) => {
     ],
     ],
     function: [
     function: [
       body('isEnabledTimeline').isBoolean(),
       body('isEnabledTimeline').isBoolean(),
-      body('isSavedStatesOfTabChanges').isBoolean(),
       body('isEnabledAttachTitleHeader').isBoolean(),
       body('isEnabledAttachTitleHeader').isBoolean(),
       body('pageLimitationS').isInt().isInt({ min: 1, max: 1000 }),
       body('pageLimitationS').isInt().isInt({ min: 1, max: 1000 }),
       body('pageLimitationM').isInt().isInt({ min: 1, max: 1000 }),
       body('pageLimitationM').isInt().isInt({ min: 1, max: 1000 }),
@@ -180,7 +177,6 @@ module.exports = (crowi) => {
     const customizeParams = {
     const customizeParams = {
       themeType: await crowi.configManager.getConfig('crowi', 'customize:theme'),
       themeType: await crowi.configManager.getConfig('crowi', 'customize:theme'),
       isEnabledTimeline: await crowi.configManager.getConfig('crowi', 'customize:isEnabledTimeline'),
       isEnabledTimeline: await crowi.configManager.getConfig('crowi', 'customize:isEnabledTimeline'),
-      isSavedStatesOfTabChanges: await crowi.configManager.getConfig('crowi', 'customize:isSavedStatesOfTabChanges'),
       isEnabledAttachTitleHeader: await crowi.configManager.getConfig('crowi', 'customize:isEnabledAttachTitleHeader'),
       isEnabledAttachTitleHeader: await crowi.configManager.getConfig('crowi', 'customize:isEnabledAttachTitleHeader'),
       pageLimitationS: await crowi.configManager.getConfig('crowi', 'customize:showPageLimitationS'),
       pageLimitationS: await crowi.configManager.getConfig('crowi', 'customize:showPageLimitationS'),
       pageLimitationM: await crowi.configManager.getConfig('crowi', 'customize:showPageLimitationM'),
       pageLimitationM: await crowi.configManager.getConfig('crowi', 'customize:showPageLimitationM'),
@@ -422,7 +418,6 @@ module.exports = (crowi) => {
   router.put('/function', loginRequiredStrictly, adminRequired, addActivity, validator.function, apiV3FormValidator, async(req, res) => {
   router.put('/function', loginRequiredStrictly, adminRequired, addActivity, validator.function, apiV3FormValidator, async(req, res) => {
     const requestParams = {
     const requestParams = {
       'customize:isEnabledTimeline': req.body.isEnabledTimeline,
       'customize:isEnabledTimeline': req.body.isEnabledTimeline,
-      'customize:isSavedStatesOfTabChanges': req.body.isSavedStatesOfTabChanges,
       'customize:isEnabledAttachTitleHeader': req.body.isEnabledAttachTitleHeader,
       'customize:isEnabledAttachTitleHeader': req.body.isEnabledAttachTitleHeader,
       'customize:showPageLimitationS': req.body.pageLimitationS,
       'customize:showPageLimitationS': req.body.pageLimitationS,
       'customize:showPageLimitationM': req.body.pageLimitationM,
       'customize:showPageLimitationM': req.body.pageLimitationM,
@@ -437,7 +432,6 @@ module.exports = (crowi) => {
       await crowi.configManager.updateConfigsInTheSameNamespace('crowi', requestParams);
       await crowi.configManager.updateConfigsInTheSameNamespace('crowi', requestParams);
       const customizedParams = {
       const customizedParams = {
         isEnabledTimeline: await crowi.configManager.getConfig('crowi', 'customize:isEnabledTimeline'),
         isEnabledTimeline: await crowi.configManager.getConfig('crowi', 'customize:isEnabledTimeline'),
-        isSavedStatesOfTabChanges: await crowi.configManager.getConfig('crowi', 'customize:isSavedStatesOfTabChanges'),
         isEnabledAttachTitleHeader: await crowi.configManager.getConfig('crowi', 'customize:isEnabledAttachTitleHeader'),
         isEnabledAttachTitleHeader: await crowi.configManager.getConfig('crowi', 'customize:isEnabledAttachTitleHeader'),
         pageLimitationS: await crowi.configManager.getConfig('crowi', 'customize:showPageLimitationS'),
         pageLimitationS: await crowi.configManager.getConfig('crowi', 'customize:showPageLimitationS'),
         pageLimitationM: await crowi.configManager.getConfig('crowi', 'customize:showPageLimitationM'),
         pageLimitationM: await crowi.configManager.getConfig('crowi', 'customize:showPageLimitationM'),

+ 5 - 1
packages/app/src/stores/context.tsx

@@ -196,7 +196,7 @@ export const useHasParent = (initialData?: boolean) : SWRResponse<boolean, Error
 };
 };
 
 
 export const useIsIndentSizeForced = (initialData?: boolean) : SWRResponse<boolean, Error> => {
 export const useIsIndentSizeForced = (initialData?: boolean) : SWRResponse<boolean, Error> => {
-  return useStaticSWR<boolean, Error>('isIndentSizeForced', initialData);
+  return useStaticSWR<boolean, Error>('isIndentSizeForced', initialData, { fallbackData: false });
 };
 };
 
 
 export const useDefaultIndentSize = (initialData?: number) : SWRResponse<number, Error> => {
 export const useDefaultIndentSize = (initialData?: number) : SWRResponse<number, Error> => {
@@ -255,6 +255,10 @@ export const useIsUploadableFile = (initialData?: boolean): SWRResponse<boolean,
   return useStaticSWR('isUploadableFile', initialData);
   return useStaticSWR('isUploadableFile', initialData);
 };
 };
 
 
+export const useShowPageLimitationL = (initialData?: number): SWRResponse<number, Error> => {
+  return useStaticSWR('showPageLimitationL', initialData);
+};
+
 export const useShowPageLimitationXL = (initialData?: number): SWRResponse<number, Error> => {
 export const useShowPageLimitationXL = (initialData?: number): SWRResponse<number, Error> => {
   return useStaticSWR('showPageLimitationXL', initialData);
   return useStaticSWR('showPageLimitationXL', initialData);
 };
 };

+ 4 - 5
packages/app/src/stores/editor.tsx

@@ -1,4 +1,4 @@
-import { Nullable } from '@growi/core';
+import { Nullable, withUtils, SWRResponseWithUtils } from '@growi/core';
 import useSWR, { SWRResponse } from 'swr';
 import useSWR, { SWRResponse } from 'swr';
 import useSWRImmutable from 'swr/immutable';
 import useSWRImmutable from 'swr/immutable';
 
 
@@ -20,7 +20,7 @@ type EditorSettingsOperation = {
   turnOffAskingBeforeDownloadLargeFiles: () => void,
   turnOffAskingBeforeDownloadLargeFiles: () => void,
 }
 }
 
 
-export const useEditorSettings = (): SWRResponse<IEditorSettings, Error> & EditorSettingsOperation => {
+export const useEditorSettings = (): SWRResponseWithUtils<EditorSettingsOperation, IEditorSettings, Error> => {
   const { data: currentUser } = useCurrentUser();
   const { data: currentUser } = useCurrentUser();
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isGuestUser } = useIsGuestUser();
 
 
@@ -30,8 +30,7 @@ export const useEditorSettings = (): SWRResponse<IEditorSettings, Error> & Edito
     { use: [localStorageMiddleware] }, // store to localStorage for initialization fastly
     { use: [localStorageMiddleware] }, // store to localStorage for initialization fastly
   );
   );
 
 
-  return {
-    ...swrResult,
+  return withUtils<EditorSettingsOperation, IEditorSettings, Error>(swrResult, {
     update: (updateData) => {
     update: (updateData) => {
       const { data, mutate } = swrResult;
       const { data, mutate } = swrResult;
 
 
@@ -56,7 +55,7 @@ export const useEditorSettings = (): SWRResponse<IEditorSettings, Error> & Edito
       // revalidate
       // revalidate
       mutate();
       mutate();
     },
     },
-  };
+  });
 };
 };
 
 
 export const useIsTextlintEnabled = (): SWRResponse<boolean, Error> => {
 export const useIsTextlintEnabled = (): SWRResponse<boolean, Error> => {

+ 17 - 7
packages/app/src/stores/page-listing.tsx

@@ -44,11 +44,21 @@ export const useSWRInifinitexRecentlyUpdated = () : SWRInfiniteResponse<(IPageHa
   );
   );
 };
 };
 // eslint-disable-next-line @typescript-eslint/no-unused-vars
 // eslint-disable-next-line @typescript-eslint/no-unused-vars
-export const useSWRxPageList = (path: string | null, pageNumber?: number, termNumber?: number): SWRResponse<IPagingResult<IPageHasId>, Error> => {
-
-  const key = path != null
-    ? [`/pages/list?path=${path}&page=${pageNumber ?? 1}`, termNumber]
-    : null;
+export const useSWRxPageList = (
+    path: string | null, pageNumber?: number, termNumber?: number, limit?: number,
+): SWRResponse<IPagingResult<IPageHasId>, Error> => {
+
+  let key;
+  // if path not exist then the key is null
+  if (path == null) {
+    key = null;
+  }
+  else {
+    const pageListPath = `/pages/list?path=${path}&page=${pageNumber ?? 1}`;
+    // if limit exist then add it as query string
+    const requestPath = limit == null ? pageListPath : `${pageListPath}&limit=${limit}`;
+    key = [requestPath, termNumber];
+  }
 
 
   return useSWR(
   return useSWR(
     key,
     key,
@@ -66,7 +76,7 @@ export const useDescendantsPageListForCurrentPathTermManager = (isDisabled?: boo
   return useTermNumberManager(isDisabled === true ? null : 'descendantsPageListForCurrentPathTermNumber');
   return useTermNumberManager(isDisabled === true ? null : 'descendantsPageListForCurrentPathTermNumber');
 };
 };
 
 
-export const useSWRxDescendantsPageListForCurrrentPath = (pageNumber?: number): SWRResponse<IPagingResult<IPageHasId>, Error> => {
+export const useSWRxDescendantsPageListForCurrrentPath = (pageNumber?: number, limit?:number): SWRResponse<IPagingResult<IPageHasId>, Error> => {
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: termNumber } = useDescendantsPageListForCurrentPathTermManager();
   const { data: termNumber } = useDescendantsPageListForCurrentPathTermManager();
 
 
@@ -74,7 +84,7 @@ export const useSWRxDescendantsPageListForCurrrentPath = (pageNumber?: number):
     ? null
     ? null
     : currentPagePath;
     : currentPagePath;
 
 
-  return useSWRxPageList(path, pageNumber, termNumber);
+  return useSWRxPageList(path, pageNumber, termNumber, limit);
 };
 };
 
 
 
 

+ 1 - 1
packages/app/src/stores/page.tsx

@@ -99,9 +99,9 @@ export const useSWRxPageInfo = (
 };
 };
 
 
 export const useSWRxPageRevisions = (
 export const useSWRxPageRevisions = (
-    pageId: string,
     page: number, // page number of pagination
     page: number, // page number of pagination
     limit: number, // max number of pages in one paginate
     limit: number, // max number of pages in one paginate
+    pageId: string | null | undefined,
 ): SWRResponse<IRevisionsForPagination, Error> => {
 ): SWRResponse<IRevisionsForPagination, Error> => {
 
 
   return useSWRImmutable<IRevisionsForPagination, Error>(
   return useSWRImmutable<IRevisionsForPagination, Error>(

+ 34 - 26
packages/app/src/stores/ui.tsx

@@ -1,10 +1,10 @@
-import { RefObject } from 'react';
+import { RefObject, useEffect } from 'react';
 
 
 import {
 import {
   isClient, isServer, pagePathUtils, Nullable, PageGrant,
   isClient, isServer, pagePathUtils, Nullable, PageGrant,
 } from '@growi/core';
 } from '@growi/core';
 import { withUtils, SWRResponseWithUtils } from '@growi/core/src/utils/with-utils';
 import { withUtils, SWRResponseWithUtils } from '@growi/core/src/utils/with-utils';
-import { Breakpoint, addBreakpointListener } from '@growi/ui';
+import { Breakpoint, addBreakpointListener, cleanupBreakpointListener } from '@growi/ui';
 import SimpleBar from 'simplebar-react';
 import SimpleBar from 'simplebar-react';
 import {
 import {
   useSWRConfig, SWRResponse, Key, Fetcher,
   useSWRConfig, SWRResponse, Key, Fetcher,
@@ -177,21 +177,25 @@ export const useIsDeviceSmallerThanMd = (): SWRResponse<boolean, Error> => {
 
 
   const { cache, mutate } = useSWRConfig();
   const { cache, mutate } = useSWRConfig();
 
 
-  if (isClient()) {
-    const mdOrAvobeHandler = function(this: MediaQueryList): void {
-      // sm -> md: matches will be true
-      // md -> sm: matches will be false
-      mutate(key, !this.matches);
-    };
-    const mql = addBreakpointListener(Breakpoint.MD, mdOrAvobeHandler);
+  useEffect(() => {
+    if (isClient()) {
+      const mdOrAvobeHandler = function(this: MediaQueryList): void {
+        // sm -> md: matches will be true
+        // md -> sm: matches will be false
+        mutate(key, !this.matches);
+      };
+      const mql = addBreakpointListener(Breakpoint.MD, mdOrAvobeHandler);
 
 
-    // initialize
-    if (cache.get(key) == null) {
-      document.addEventListener('DOMContentLoaded', () => {
+      // initialize
+      if (cache.get(key) == null) {
         mutate(key, !mql.matches);
         mutate(key, !mql.matches);
-      });
+      }
+
+      return () => {
+        cleanupBreakpointListener(mql, mdOrAvobeHandler);
+      };
     }
     }
-  }
+  }, [cache, key, mutate]);
 
 
   return useStaticSWR(key);
   return useStaticSWR(key);
 };
 };
@@ -201,21 +205,25 @@ export const useIsDeviceSmallerThanLg = (): SWRResponse<boolean, Error> => {
 
 
   const { cache, mutate } = useSWRConfig();
   const { cache, mutate } = useSWRConfig();
 
 
-  if (isClient()) {
-    const lgOrAvobeHandler = function(this: MediaQueryList): void {
-      // md -> lg: matches will be true
-      // lg -> md: matches will be false
-      mutate(key, !this.matches);
-    };
-    const mql = addBreakpointListener(Breakpoint.LG, lgOrAvobeHandler);
+  useEffect(() => {
+    if (isClient()) {
+      const lgOrAvobeHandler = function(this: MediaQueryList): void {
+        // md -> lg: matches will be true
+        // lg -> md: matches will be false
+        mutate(key, !this.matches);
+      };
+      const mql = addBreakpointListener(Breakpoint.LG, lgOrAvobeHandler);
 
 
-    // initialize
-    if (cache.get(key) == null) {
-      document.addEventListener('DOMContentLoaded', () => {
+      // initialize
+      if (cache.get(key) == null) {
         mutate(key, !mql.matches);
         mutate(key, !mql.matches);
-      });
+      }
+
+      return () => {
+        cleanupBreakpointListener(mql, lgOrAvobeHandler);
+      };
     }
     }
-  }
+  }, [cache, key, mutate]);
 
 
   return useStaticSWR(key);
   return useStaticSWR(key);
 };
 };

+ 0 - 68
packages/app/src/styles/_page-history.scss

@@ -1,68 +0,0 @@
-// @import '../scss/variables';
-// @import '../scss/override-bootstrap-variables';
-
-.revision-history-table {
-  tbody {
-    max-height: 250px;
-  }
-}
-
-.revision-history-main {
-  img.picture-lg {
-    width: 32px;
-    height: 32px;
-  }
-}
-
-.revision-history-main-nodiff {
-  .picture-container {
-    min-width: 32px;
-    text-align: center; // centering .picture
-  }
-}
-
-.revision-history-diff {
-  color: $gray-900;
-  table-layout: fixed;
-
-  // revision-history
-  // to stay d2h-code-side-line-number in the revision history diff area
-  .d2h-wrapper {
-    position: relative;
-  }
-}
-
-.comparison-header {
-  height: 34px;
-  background-color: #ffffff;
-  border: 1px solid $gray-300;
-  .comparison-source-wrapper {
-    height: 26px;
-    margin-right: 1px;
-    border-right: 1px solid $gray-300;
-    .comparison-source {
-      color: $gray-500;
-    }
-  }
-  .comparison-target-wrapper {
-    height: 26px;
-    .comparison-target {
-      color: $gray-500;
-    }
-  }
-}
-
-.revision-compare {
-  .revision-compare-container {
-    min-height: 100px;
-
-    &.nodiff {
-      display: flex;
-      align-items: center;
-      justify-content: center;
-    }
-  }
-  .d2h-file-header {
-    display: none;
-  }
-}

+ 2 - 2
packages/app/src/styles/_page.scss

@@ -1,5 +1,5 @@
-// import diff2html styles
-@import '~diff2html/bundles/css/diff2html.min.css';
+// // import diff2html styles
+// @import '~/diff2html/bundles/css/diff2html.min.css';
 
 
 /**
 /**
  * for table with handsontable modal button
  * for table with handsontable modal button

+ 1 - 1
packages/codemirror-textlint/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/codemirror-textlint",
   "name": "@growi/codemirror-textlint",
-  "version": "6.0.0-RC.1",
+  "version": "6.0.0-RC.3",
   "license": "MIT",
   "license": "MIT",
   "main": "dist/index.js",
   "main": "dist/index.js",
   "scripts": {
   "scripts": {

+ 1 - 1
packages/core/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/core",
   "name": "@growi/core",
-  "version": "6.0.0-RC.1",
+  "version": "6.0.0-RC.3",
   "description": "GROWI Core Libraries",
   "description": "GROWI Core Libraries",
   "license": "MIT",
   "license": "MIT",
   "keywords": [
   "keywords": [

+ 7 - 1
packages/core/src/interfaces/revision.ts

@@ -11,8 +11,14 @@ export type IRevision = {
 
 
 export type IRevisionHasId = IRevision & HasObjectId;
 export type IRevisionHasId = IRevision & HasObjectId;
 
 
+type HasPageId = {
+  pageId: string,
+};
+
+export type IRevisionHasPageId = IRevisionHasId & HasPageId;
+
 export type IRevisionsForPagination = {
 export type IRevisionsForPagination = {
-  revisions: IRevision[], // revisions in one pagination
+  revisions: IRevisionHasPageId[], // revisions in one pagination
   totalCounts: number // total counts
   totalCounts: number // total counts
 }
 }
 
 

+ 1 - 1
packages/plugin-attachment-refs/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/plugin-attachment-refs",
   "name": "@growi/plugin-attachment-refs",
-  "version": "6.0.0-RC.1",
+  "version": "6.0.0-RC.3",
   "description": "GROWI Plugin to add ref/refimg/refs/refsimg tags",
   "description": "GROWI Plugin to add ref/refimg/refs/refsimg tags",
   "license": "MIT",
   "license": "MIT",
   "keywords": [
   "keywords": [

+ 4 - 4
packages/plugin-lsx/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/plugin-lsx",
   "name": "@growi/plugin-lsx",
-  "version": "6.0.0-RC.1",
+  "version": "6.0.0-RC.3",
   "description": "GROWI plugin to list pages",
   "description": "GROWI plugin to list pages",
   "license": "MIT",
   "license": "MIT",
   "keywords": ["growi", "growi-plugin"],
   "keywords": ["growi", "growi-plugin"],
@@ -23,9 +23,9 @@
     "test": ""
     "test": ""
   },
   },
   "dependencies": {
   "dependencies": {
-    "@growi/core": "^6.0.0-RC.1",
-    "@growi/remark-growi-plugin": "^6.0.0-RC.1",
-    "@growi/ui": "^6.0.0-RC.1"
+    "@growi/core": "^6.0.0-RC.3",
+    "@growi/remark-growi-plugin": "^6.0.0-RC.3",
+    "@growi/ui": "^6.0.0-RC.3"
   },
   },
   "devDependencies": {
   "devDependencies": {
     "eslint-plugin-regex": "^1.8.0",
     "eslint-plugin-regex": "^1.8.0",

+ 1 - 1
packages/remark-growi-plugin/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/remark-growi-plugin",
   "name": "@growi/remark-growi-plugin",
-  "version": "6.0.0-RC.1",
+  "version": "6.0.0-RC.3",
   "description": "remark plugin to support GROWI plugin (forked from remark-directive@2.0.1)",
   "description": "remark plugin to support GROWI plugin (forked from remark-directive@2.0.1)",
   "license": "MIT",
   "license": "MIT",
   "keywords": [
   "keywords": [

+ 1 - 1
packages/slack/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/slack",
   "name": "@growi/slack",
-  "version": "6.0.0-RC.1",
+  "version": "6.0.0-RC.3",
   "license": "MIT",
   "license": "MIT",
   "main": "dist/index.js",
   "main": "dist/index.js",
   "typings": "dist/index.d.ts",
   "typings": "dist/index.d.ts",

+ 2 - 2
packages/slackbot-proxy/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/slackbot-proxy",
   "name": "@growi/slackbot-proxy",
-  "version": "6.0.0-slackbot-proxy.1",
+  "version": "6.0.0-RC.3",
   "license": "MIT",
   "license": "MIT",
   "scripts": {
   "scripts": {
     "build": "yarn tsc && tsc-alias -p tsconfig.build.json",
     "build": "yarn tsc && tsc-alias -p tsconfig.build.json",
@@ -26,7 +26,7 @@
   },
   },
   "dependencies": {
   "dependencies": {
     "@godaddy/terminus": "^4.9.0",
     "@godaddy/terminus": "^4.9.0",
-    "@growi/slack": "^6.0.0-RC.1",
+    "@growi/slack": "^6.0.0-RC.3",
     "@slack/oauth": "^2.0.1",
     "@slack/oauth": "^2.0.1",
     "@slack/web-api": "^6.2.4",
     "@slack/web-api": "^6.2.4",
     "@tsed/common": "^6.43.0",
     "@tsed/common": "^6.43.0",

+ 2 - 2
packages/ui/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/ui",
   "name": "@growi/ui",
-  "version": "6.0.0-RC.1",
+  "version": "6.0.0-RC.3",
   "description": "GROWI UI Libraries",
   "description": "GROWI UI Libraries",
   "license": "MIT",
   "license": "MIT",
   "keywords": ["growi"],
   "keywords": ["growi"],
@@ -17,7 +17,7 @@
     "test": "jest --verbose"
     "test": "jest --verbose"
   },
   },
   "dependencies": {
   "dependencies": {
-    "@growi/core": "^6.0.0-RC.1"
+    "@growi/core": "^6.0.0-RC.3"
   },
   },
   "devDependencies": {
   "devDependencies": {
     "eslint-plugin-regex": "^1.8.0",
     "eslint-plugin-regex": "^1.8.0",

+ 10 - 1
packages/ui/src/utils/browser-utils.ts

@@ -1,5 +1,6 @@
 import { Breakpoint } from '../interfaces/breakpoints';
 import { Breakpoint } from '../interfaces/breakpoints';
 
 
+const EVENT_TYPE_CHANGE = 'change';
 
 
 export const addBreakpointListener = (
 export const addBreakpointListener = (
     breakpoint: Breakpoint,
     breakpoint: Breakpoint,
@@ -12,7 +13,15 @@ export const addBreakpointListener = (
   const mediaQueryList = window.matchMedia(`(min-width: ${breakpointPixel}px)`);
   const mediaQueryList = window.matchMedia(`(min-width: ${breakpointPixel}px)`);
 
 
   // add event listener
   // add event listener
-  mediaQueryList.addEventListener('change', listener);
+  mediaQueryList.addEventListener(EVENT_TYPE_CHANGE, listener);
 
 
   return mediaQueryList;
   return mediaQueryList;
 };
 };
+
+export const cleanupBreakpointListener = (
+    mediaQueryList: MediaQueryList,
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    listener: (this: MediaQueryList, ev: MediaQueryListEvent) => any,
+): void => {
+  mediaQueryList.removeEventListener(EVENT_TYPE_CHANGE, listener);
+};