Browse Source

Merge branch 'master' into feat/isolate-hackmd-from-app

Yuken Tezuka 3 years ago
parent
commit
cad88bf6dc
45 changed files with 548 additions and 535 deletions
  1. 0 11
      packages/app/src/client/services/AdminCustomizeContainer.js
  2. 0 14
      packages/app/src/client/services/page-operation.ts
  3. 5 5
      packages/app/src/components/Admin/Customize/CustomizeCssSetting.tsx
  4. 19 34
      packages/app/src/components/Admin/Customize/CustomizeFunctionSetting.tsx
  5. 4 4
      packages/app/src/components/Admin/Customize/CustomizeHeaderSetting.tsx
  6. 4 4
      packages/app/src/components/Admin/Customize/CustomizeHighlightSetting.tsx
  7. 5 18
      packages/app/src/components/Admin/Customize/CustomizeLayoutSetting.tsx
  8. 9 9
      packages/app/src/components/Admin/Customize/CustomizeLogoSetting.tsx
  9. 5 5
      packages/app/src/components/Admin/Customize/CustomizeScriptSetting.tsx
  10. 2 2
      packages/app/src/components/Admin/Customize/CustomizeThemeSetting.tsx
  11. 1 1
      packages/app/src/components/Admin/Customize/CustomizeTitle.tsx
  12. 4 0
      packages/app/src/components/Layout/SearchResultLayout.module.scss
  13. 5 2
      packages/app/src/components/Navbar/GrowiSubNavigation.tsx
  14. 4 3
      packages/app/src/components/PageAccessoriesModal.tsx
  15. 6 2
      packages/app/src/components/PageCreateModal.jsx
  16. 16 12
      packages/app/src/components/PageHistory.tsx
  17. 5 0
      packages/app/src/components/PageHistory/PageRevisionTable.module.scss
  18. 62 86
      packages/app/src/components/PageHistory/PageRevisionTable.tsx
  19. 0 88
      packages/app/src/components/PageHistory/Revision.jsx
  20. 13 0
      packages/app/src/components/PageHistory/Revision.module.scss
  21. 77 0
      packages/app/src/components/PageHistory/Revision.tsx
  22. 0 87
      packages/app/src/components/PageHistory/RevisionDiff.jsx
  23. 35 0
      packages/app/src/components/PageHistory/RevisionDiff.module.scss
  24. 67 0
      packages/app/src/components/PageHistory/RevisionDiff.tsx
  25. 4 3
      packages/app/src/components/PagePathNav.tsx
  26. 3 1
      packages/app/src/components/PrivateLegacyPages.tsx
  27. 14 0
      packages/app/src/components/RevisionComparer/RevisionComparer.module.scss
  28. 21 33
      packages/app/src/components/RevisionComparer/RevisionComparer.tsx
  29. 4 0
      packages/app/src/interfaces/customize.ts
  30. 35 0
      packages/app/src/migrations/20221014130200-remove-customize-is-saved-states-of-tab-changes.js
  31. 20 8
      packages/app/src/pages/[[...path]].page.tsx
  32. 28 14
      packages/app/src/pages/_app.page.tsx
  33. 12 0
      packages/app/src/pages/utils/layout.ts
  34. 0 3
      packages/app/src/server/models/config.ts
  35. 0 6
      packages/app/src/server/routes/apiv3/customize-setting.js
  36. 1 1
      packages/app/src/server/service/page.ts
  37. 27 0
      packages/app/src/stores/admin/customize.tsx
  38. 5 0
      packages/app/src/stores/context.tsx
  39. 1 1
      packages/app/src/stores/page.tsx
  40. 9 5
      packages/app/src/stores/use-static-swr.tsx
  41. 0 68
      packages/app/src/styles/_page-history.scss
  42. 2 2
      packages/app/src/styles/_page.scss
  43. 6 2
      packages/app/src/utils/swr-utils.ts
  44. 1 0
      packages/core/src/interfaces/page.ts
  45. 7 1
      packages/core/src/interfaces/revision.ts

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

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

+ 0 - 14
packages/app/src/client/services/page-operation.ts

@@ -42,23 +42,9 @@ export const toggleBookmark = async(pageId: string, currentValue?: boolean): Pro
   }
 };
 
-// Utility to update body class
-const updateBodyClassByView = (expandContentWidth: boolean): void => {
-  const bodyClasses = document.body.classList;
-  const isLayoutFluid = bodyClasses.contains('growi-layout-fluid');
-
-  if (expandContentWidth && !isLayoutFluid) {
-    bodyClasses.add('growi-layout-fluid');
-  }
-  else if (isLayoutFluid) {
-    bodyClasses.remove('growi-layout-fluid');
-  }
-};
-
 export const updateContentWidth = async(pageId: string, newValue: boolean): Promise<void> => {
   try {
     await apiv3Put(`/page/${pageId}/content-width`, { expandContentWidth: newValue });
-    updateBodyClassByView(newValue);
   }
   catch (err) {
     toastError(err);

+ 5 - 5
packages/app/src/components/Admin/Customize/CustomizeCssSetting.tsx

@@ -21,7 +21,7 @@ const CustomizeCssSetting = (props: Props): JSX.Element => {
   const onClickSubmit = useCallback(async() => {
     try {
       await adminCustomizeContainer.updateCustomizeCss();
-      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_setting.custom_css') }));
+      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.custom_css') }));
     }
     catch (err) {
       toastError(err);
@@ -32,12 +32,12 @@ const CustomizeCssSetting = (props: Props): JSX.Element => {
     <React.Fragment>
       <div className="row">
         <div className="col-12">
-          <h2 className="admin-setting-header">{t('admin:customize_setting.custom_css')}</h2>
+          <h2 className="admin-setting-header">{t('admin:customize_settings.custom_css')}</h2>
 
           <Card className="card well my-3">
             <CardBody className="px-0 py-2">
-              { t('admin:customize_setting.write_css') }<br />
-              { t('admin:customize_setting.reflect_change') }
+              { t('admin:customize_settings.write_css') }<br />
+              { t('admin:customize_settings.reflect_change') }
             </CardBody>
           </Card>
 
@@ -50,7 +50,7 @@ const CustomizeCssSetting = (props: Props): JSX.Element => {
             />
             <p className="form-text text-muted text-right">
               <i className="fa fa-fw fa-keyboard-o" aria-hidden="true" />
-              {t('admin:customize_setting.ctrl_space')}
+              {t('admin:customize_settings.ctrl_space')}
             </p>
           </div>
 

+ 19 - 34
packages/app/src/components/Admin/Customize/CustomizeFunctionSetting.tsx

@@ -25,7 +25,7 @@ const CustomizeFunctionSetting = (props: Props): JSX.Element => {
 
     try {
       await adminCustomizeContainer.updateCustomizeFunction();
-      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_setting.function') }));
+      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.function') }));
     }
     catch (err) {
       toastError(err);
@@ -36,68 +36,53 @@ const CustomizeFunctionSetting = (props: Props): JSX.Element => {
     <React.Fragment>
       <div className="row">
         <div className="col-12">
-          <h2 className="admin-setting-header">{t('admin:customize_setting.function')}</h2>
+          <h2 className="admin-setting-header">{t('admin:customize_settings.function')}</h2>
           <Card className="card well my-3">
             <CardBody className="px-0 py-2">
-              {t('admin:customize_setting.function_desc')}
+              {t('admin:customize_settings.function_desc')}
             </CardBody>
           </Card>
 
 
-          <div className="form-group row">
-            <div className="offset-md-3 col-md-6 text-left">
-              <CustomizeFunctionOption
-                optionId="isSavedStatesOfTabChanges"
-                label={t('admin:customize_setting.function_options.tab_switch')}
-                isChecked={adminCustomizeContainer.state.isSavedStatesOfTabChanges}
-                onChecked={() => { adminCustomizeContainer.switchSavedStatesOfTabChanges() }}
-              >
-                <p className="form-text text-muted">
-                  {t('admin:customize_setting.function_options.tab_switch_desc1')}<br />
-                  {t('admin:customize_setting.function_options.tab_switch_desc2')}
-                </p>
-              </CustomizeFunctionOption>
-            </div>
-          </div>
           <div className="form-group row">
             <div className="offset-md-3 col-md-6 text-left">
               <CustomizeFunctionOption
                 optionId="isEnabledAttachTitleHeader"
-                label={t('admin:customize_setting.function_options.attach_title_header')}
+                label={t('admin:customize_settings.function_options.attach_title_header')}
                 isChecked={adminCustomizeContainer.state.isEnabledAttachTitleHeader}
                 onChecked={() => { adminCustomizeContainer.switchEnabledAttachTitleHeader() }}
               >
                 <p className="form-text text-muted">
-                  {t('admin:customize_setting.function_options.attach_title_header_desc')}
+                  {t('admin:customize_settings.function_options.attach_title_header_desc')}
                 </p>
               </CustomizeFunctionOption>
             </div>
           </div>
 
           <PagingSizeUncontrolledDropdown
-            label={t('admin:customize_setting.function_options.list_num_s')}
-            desc={t('admin:customize_setting.function_options.list_num_desc_s')}
+            label={t('admin:customize_settings.function_options.list_num_s')}
+            desc={t('admin:customize_settings.function_options.list_num_desc_s')}
             toggleLabel={adminCustomizeContainer.state.pageLimitationS || 20}
             dropdownItemSize={[10, 20, 50, 100]}
             onChangeDropdownItem={adminCustomizeContainer.switchPageListLimitationS}
           />
           <PagingSizeUncontrolledDropdown
-            label={t('admin:customize_setting.function_options.list_num_m')}
-            desc={t('admin:customize_setting.function_options.list_num_desc_m')}
+            label={t('admin:customize_settings.function_options.list_num_m')}
+            desc={t('admin:customize_settings.function_options.list_num_desc_m')}
             toggleLabel={adminCustomizeContainer.state.pageLimitationM || 10}
             dropdownItemSize={[5, 10, 20, 50, 100]}
             onChangeDropdownItem={adminCustomizeContainer.switchPageListLimitationM}
           />
           <PagingSizeUncontrolledDropdown
-            label={t('admin:customize_setting.function_options.list_num_l')}
-            desc={t('admin:customize_setting.function_options.list_num_desc_l')}
+            label={t('admin:customize_settings.function_options.list_num_l')}
+            desc={t('admin:customize_settings.function_options.list_num_desc_l')}
             toggleLabel={adminCustomizeContainer.state.pageLimitationL || 50}
             dropdownItemSize={[20, 50, 100, 200]}
             onChangeDropdownItem={adminCustomizeContainer.switchPageListLimitationL}
           />
           <PagingSizeUncontrolledDropdown
-            label={t('admin:customize_setting.function_options.list_num_xl')}
-            desc={t('admin:customize_setting.function_options.list_num_desc_xl')}
+            label={t('admin:customize_settings.function_options.list_num_xl')}
+            desc={t('admin:customize_settings.function_options.list_num_desc_xl')}
             toggleLabel={adminCustomizeContainer.state.pageLimitationXL || 20}
             dropdownItemSize={[5, 10, 20, 50, 100]}
             onChangeDropdownItem={adminCustomizeContainer.switchPageListLimitationXL}
@@ -107,12 +92,12 @@ const CustomizeFunctionSetting = (props: Props): JSX.Element => {
             <div className="offset-md-3 col-md-6 text-left">
               <CustomizeFunctionOption
                 optionId="isEnabledStaleNotification"
-                label={t('admin:customize_setting.function_options.stale_notification')}
+                label={t('admin:customize_settings.function_options.stale_notification')}
                 isChecked={adminCustomizeContainer.state.isEnabledStaleNotification}
                 onChecked={() => { adminCustomizeContainer.switchEnableStaleNotification() }}
               >
                 <p className="form-text text-muted">
-                  {t('admin:customize_setting.function_options.stale_notification_desc')}
+                  {t('admin:customize_settings.function_options.stale_notification_desc')}
                 </p>
               </CustomizeFunctionOption>
             </div>
@@ -122,12 +107,12 @@ const CustomizeFunctionSetting = (props: Props): JSX.Element => {
             <div className="offset-md-3 col-md-6 text-left">
               <CustomizeFunctionOption
                 optionId="isAllReplyShown"
-                label={t('admin:customize_setting.function_options.show_all_reply_comments')}
+                label={t('admin:customize_settings.function_options.show_all_reply_comments')}
                 isChecked={adminCustomizeContainer.state.isAllReplyShown || false}
                 onChecked={() => { adminCustomizeContainer.switchIsAllReplyShown() }}
               >
                 <p className="form-text text-muted">
-                  {t('admin:customize_setting.function_options.show_all_reply_comments_desc')}
+                  {t('admin:customize_settings.function_options.show_all_reply_comments_desc')}
                 </p>
               </CustomizeFunctionOption>
             </div>
@@ -137,12 +122,12 @@ const CustomizeFunctionSetting = (props: Props): JSX.Element => {
             <div className="offset-md-3 col-md-6 text-left">
               <CustomizeFunctionOption
                 optionId="isSearchScopeChildrenAsDefault"
-                label={t('admin:customize_setting.function_options.select_search_scope_children_as_default')}
+                label={t('admin:customize_settings.function_options.select_search_scope_children_as_default')}
                 isChecked={adminCustomizeContainer.state.isSearchScopeChildrenAsDefault || false}
                 onChecked={() => { adminCustomizeContainer.switchIsSearchScopeChildrenAsDefault() }}
               >
                 <p className="form-text text-muted">
-                  {t('admin:customize_setting.function_options.select_search_scope_children_as_default_desc')}
+                  {t('admin:customize_settings.function_options.select_search_scope_children_as_default_desc')}
                 </p>
               </CustomizeFunctionOption>
             </div>

+ 4 - 4
packages/app/src/components/Admin/Customize/CustomizeHeaderSetting.tsx

@@ -21,7 +21,7 @@ const CustomizeHeaderSetting = (props: Props): JSX.Element => {
   const onClickSubmit = useCallback(async() => {
     try {
       await adminCustomizeContainer.updateCustomizeHeader();
-      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_setting.custom_header') }));
+      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.custom_header') }));
     }
     catch (err) {
       toastError(err);
@@ -32,13 +32,13 @@ const CustomizeHeaderSetting = (props: Props): JSX.Element => {
     <React.Fragment>
       <div className="row">
         <div className="col-12">
-          <h2 className="admin-setting-header">{t('admin:customize_setting.custom_header')}</h2>
+          <h2 className="admin-setting-header">{t('admin:customize_settings.custom_header')}</h2>
 
           <Card className="card well my-3">
             <CardBody className="px-0 py-2">
               <span
                 // eslint-disable-next-line react/no-danger
-                dangerouslySetInnerHTML={{ __html: t('admin:customize_setting.custom_header_detail') }}
+                dangerouslySetInnerHTML={{ __html: t('admin:customize_settings.custom_header_detail') }}
               />
             </CardBody>
           </Card>
@@ -61,7 +61,7 @@ const CustomizeHeaderSetting = (props: Props): JSX.Element => {
             />
             <p className="form-text text-muted text-right">
               <i className="fa fa-fw fa-keyboard-o" aria-hidden="true"></i>
-              {t('admin:customize_setting.ctrl_space')}
+              {t('admin:customize_settings.ctrl_space')}
             </p>
           </div>
           <AdminUpdateButtonRow onClick={onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />

+ 4 - 4
packages/app/src/components/Admin/Customize/CustomizeHighlightSetting.tsx

@@ -59,7 +59,7 @@ const CustomizeHighlightSetting = (props: Props): JSX.Element => {
   const onClickSubmit = useCallback(async() => {
     try {
       await adminCustomizeContainer.updateHighlightJsStyle();
-      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_setting.code_highlight') }));
+      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.code_highlight') }));
     }
     catch (err) {
       toastError(err);
@@ -90,12 +90,12 @@ const CustomizeHighlightSetting = (props: Props): JSX.Element => {
     <React.Fragment>
       <div className="row">
         <div className="col-12">
-          <h2 className="admin-setting-header">{t('admin:customize_setting.code_highlight')}</h2>
+          <h2 className="admin-setting-header">{t('admin:customize_settings.code_highlight')}</h2>
 
           <div className="form-group row">
             <div className="offset-md-3 col-md-6 text-left">
               <div className="my-0">
-                <label>{t('admin:customize_setting.theme')}</label>
+                <label>{t('admin:customize_settings.theme')}</label>
               </div>
               <Dropdown isOpen={isDropdownOpen} toggle={onToggleDropdown}>
                 <DropdownToggle className="text-right col-6" caret>
@@ -107,7 +107,7 @@ const CustomizeHighlightSetting = (props: Props): JSX.Element => {
               </Dropdown>
               <p className="form-text text-warning">
                 {/* eslint-disable-next-line react/no-danger */}
-                <span dangerouslySetInnerHTML={{ __html: t('admin:customize_setting.nocdn_desc') }} />
+                <span dangerouslySetInnerHTML={{ __html: t('admin:customize_settings.nocdn_desc') }} />
               </p>
             </div>
           </div>

+ 5 - 18
packages/app/src/components/Admin/Customize/CustomizeLayoutSetting.tsx

@@ -3,37 +3,24 @@ import React, { useCallback, useEffect, useState } from 'react';
 import { useTranslation } from 'next-i18next';
 
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
-import { apiv3Get, apiv3Put } from '~/client/util/apiv3-client';
+import { apiv3Put } from '~/client/util/apiv3-client';
+import { useSWRxLayoutSetting } from '~/stores/admin/customize';
 import { useNextThemes } from '~/stores/use-next-themes';
 
 const CustomizeLayoutSetting = (): JSX.Element => {
   const { t } = useTranslation('admin');
 
   const { resolvedTheme } = useNextThemes();
+  const { data: layoutSetting, mutate: mutateLayoutSetting } = useSWRxLayoutSetting();
 
-  const [isContainerFluid, setIsContainerFluid] = useState(false);
+  const [isContainerFluid, setIsContainerFluid] = useState<boolean>(layoutSetting?.isContainerFluid ?? false);
   const [retrieveError, setRetrieveError] = useState<any>();
 
-  const retrieveData = useCallback(async() => {
-    try {
-      const res = await apiv3Get('/customize-setting/layout');
-      setIsContainerFluid(res.data.isContainerFluid);
-    }
-    catch (err) {
-      setRetrieveError(err);
-      toastError(err);
-    }
-  }, []);
-
-  useEffect(() => {
-    retrieveData();
-  }, [retrieveData]);
-
   const onClickSubmit = async() => {
     try {
       await apiv3Put('/customize-setting/layout', { isContainerFluid });
       toastSuccess(t('toaster.update_successed', { target: t('customize_settings.layout') }));
-      retrieveData();
+      mutateLayoutSetting();
     }
     catch (err) {
       toastError(err);

+ 9 - 9
packages/app/src/components/Admin/Customize/CustomizeLogoSetting.tsx

@@ -59,7 +59,7 @@ const CustomizeLogoSetting = (): JSX.Element => {
       const { customizedParams } = response.data;
       setIsDefaultLogo(customizedParams.isDefaultLogo);
       setCustomizedLogoSrc(customizedParams.customizedLogoSrc);
-      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_setting.custom_logo') }));
+      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.custom_logo') }));
     }
     catch (err) {
       toastError(err);
@@ -70,7 +70,7 @@ const CustomizeLogoSetting = (): JSX.Element => {
     try {
       await apiv3Delete('/customize-setting/delete-brand-logo');
       setCustomizedLogoSrc(null);
-      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_setting.current_logo') }));
+      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.current_logo') }));
     }
     catch (err) {
       toastError(err);
@@ -86,7 +86,7 @@ const CustomizeLogoSetting = (): JSX.Element => {
       formData.append('file', croppedImage);
       const { data } = await apiv3PostForm('/customize-setting/upload-brand-logo', formData);
       setCustomizedLogoSrc(data.attachment.filePathProxied);
-      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_setting.current_logo') }));
+      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.current_logo') }));
     }
     catch (err) {
       toastError(err);
@@ -100,7 +100,7 @@ const CustomizeLogoSetting = (): JSX.Element => {
       <div className="row">
         <div className="col-12">
           <div className="mb-5">
-            <h2 className="border-bottom my-4 admin-setting-header">{t('admin:customize_setting.custom_logo')}</h2>
+            <h2 className="border-bottom my-4 admin-setting-header">{t('admin:customize_settings.custom_logo')}</h2>
             <div className="row">
               <div className="col-md-6 col-12 mb-3 mb-md-0">
                 <h4>
@@ -115,7 +115,7 @@ const CustomizeLogoSetting = (): JSX.Element => {
                       onChange={() => { setIsDefaultLogo(true) }}
                     />
                     <label className="custom-control-label" htmlFor="radioDefaultLogo">
-                      {t('admin:customize_setting.default_logo')}
+                      {t('admin:customize_settings.default_logo')}
                     </label>
                   </div>
                 </h4>
@@ -134,26 +134,26 @@ const CustomizeLogoSetting = (): JSX.Element => {
                       onChange={() => { setIsDefaultLogo(false) }}
                     />
                     <label className="custom-control-label" htmlFor="radioUploadLogo">
-                      { t('admin:customize_setting.upload_logo') }
+                      { t('admin:customize_settings.upload_logo') }
                     </label>
                   </div>
                 </h4>
                 <div className="row mb-3">
                   <label className="col-sm-4 col-12 col-form-label text-left">
-                    { t('admin:customize_setting.current_logo') }
+                    { t('admin:customize_settings.current_logo') }
                   </label>
                   <div className="col-sm-8 col-12">
                     <p><img src={customizedLogoSrc || DEFAULT_LOGO} className="picture picture-lg " id="settingBrandLogo" width="64" /></p>
                     {(customizedLogoSrc != null) && (
                       <button type="button" className="btn btn-danger" onClick={onClickDeleteBtn}>
-                        { t('admin:customize_setting.delete_logo') }
+                        { t('admin:customize_settings.delete_logo') }
                       </button>
                     )}
                   </div>
                 </div>
                 <div className="row">
                   <label className="col-sm-4 col-12 col-form-label text-left">
-                    { t('admin:customize_setting.upload_new_logo') }
+                    { t('admin:customize_settings.upload_new_logo') }
                   </label>
                   <div className="col-sm-8 col-12">
                     <input type="file" onChange={onSelectFile} name="brandLogo" accept="image/*" />

+ 5 - 5
packages/app/src/components/Admin/Customize/CustomizeScriptSetting.tsx

@@ -21,7 +21,7 @@ const CustomizeScriptSetting = (props: Props): JSX.Element => {
   const onClickSubmit = useCallback(async() => {
     try {
       await adminCustomizeContainer.updateCustomizeScript();
-      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_setting.custom_script') }));
+      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.custom_script') }));
     }
     catch (err) {
       toastError(err);
@@ -40,11 +40,11 @@ const CustomizeScriptSetting = (props: Props): JSX.Element => {
     <React.Fragment>
       <div className="row">
         <div className="col-12">
-          <h2 className="admin-setting-header">{t('admin:customize_setting.custom_script')}</h2>
+          <h2 className="admin-setting-header">{t('admin:customize_settings.custom_script')}</h2>
           <Card className="card well">
             <CardBody className="px-0 py-2">
-              {t('admin:customize_setting.write_java')}<br />
-              {t('admin:customize_setting.reflect_change')}
+              {t('admin:customize_settings.write_java')}<br />
+              {t('admin:customize_settings.reflect_change')}
             </CardBody>
           </Card>
 
@@ -91,7 +91,7 @@ const CustomizeScriptSetting = (props: Props): JSX.Element => {
             />
             <p className="form-text text-muted text-right">
               <i className="fa fa-fw fa-keyboard-o" aria-hidden="true" />
-              {t('admin:customize_setting.ctrl_space')}
+              {t('admin:customize_settings.ctrl_space')}
             </p>
           </div>
 

+ 2 - 2
packages/app/src/components/Admin/Customize/CustomizeThemeSetting.tsx

@@ -35,7 +35,7 @@ const CustomizeThemeSetting = (props: Props): JSX.Element => {
         });
       }
 
-      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_setting.theme') }));
+      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.theme') }));
     }
     catch (err) {
       toastError(err);
@@ -45,7 +45,7 @@ const CustomizeThemeSetting = (props: Props): JSX.Element => {
   return (
     <div className="row">
       <div className="col-12">
-        <h2 className="admin-setting-header">{t('admin:customize_setting.theme')}</h2>
+        <h2 className="admin-setting-header">{t('admin:customize_settings.theme')}</h2>
         <CustomizeThemeOptions onSelected={selectedHandler} currentTheme={currentTheme} />
         <AdminUpdateButtonRow onClick={submitHandler} disabled={adminCustomizeContainer.state.retrieveError != null} />
       </div>

+ 1 - 1
packages/app/src/components/Admin/Customize/CustomizeTitle.tsx

@@ -22,7 +22,7 @@ export const CustomizeTitle: FC = () => {
       await apiv3Put('/customize-setting/customize-title', {
         customizeTitle: currentCustomizeTitle,
       });
-      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_setting.custom_title') }));
+      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.custom_title') }));
     }
     catch (err) {
       toastError(err);

+ 4 - 0
packages/app/src/components/Layout/SearchResultLayout.module.scss

@@ -67,6 +67,10 @@
       }
 
       .search-result-content-body-container {
+        // correct apply overflow scrolling for react-markdown on Google Chrome
+        // see: https://github.com/weseek/growi/pull/6731
+        position: relative;
+
         overflow-y: auto;
 
         .wiki {

+ 5 - 2
packages/app/src/components/Navbar/GrowiSubNavigation.tsx

@@ -54,8 +54,11 @@ export const GrowiSubNavigation = (props: GrowiSubNavigationProps): JSX.Element
   const compactModeClasses = isCompactMode ? 'grw-subnav-compact d-print-none' : '';
 
   return (
-    <div className={`grw-subnav ${styles['grw-subnav']} d-flex align-items-center justify-content-between ${additionalClasses.join(' ')}
-    ${compactModeClasses}`} >
+    <div className={`
+      grw-subnav ${styles['grw-subnav']} d-flex align-items-center justify-content-between
+      ${additionalClasses.join(' ')}
+      ${compactModeClasses}`}
+    >
       {/* Left side */}
       <div className="d-flex grw-subnav-left-side">
         { (showDrawerToggler && isDrawerMode) && (

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

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

+ 6 - 2
packages/app/src/components/PageCreateModal.jsx

@@ -13,6 +13,7 @@ import { debounce } from 'throttle-debounce';
 import { toastError } from '~/client/util/apiNotification';
 import { useCurrentUser, useIsSearchServiceReachable } from '~/stores/context';
 import { usePageCreateModal } from '~/stores/modal';
+import { EditorMode, useEditorMode } from '~/stores/ui';
 
 import PagePathAutoComplete from './PagePathAutoComplete';
 
@@ -36,6 +37,8 @@ const PageCreateModal = () => {
   const pageNameInputInitialValue = isCreatable ? pathUtils.addTrailingSlash(pathname) : '/';
   const now = format(new Date(), 'yyyy/MM/dd');
 
+  const { mutate: mutateEditorMode } = useEditorMode();
+
   const [todayInput1, setTodayInput1] = useState(t('Memo'));
   const [todayInput2, setTodayInput2] = useState('');
   const [pageNameInput, setPageNameInput] = useState(pageNameInputInitialValue);
@@ -99,8 +102,9 @@ const PageCreateModal = () => {
    */
   async function redirectToEditor(...paths) {
     try {
-      const editorPath = await generateEditorPath(...paths);
-      router.push(editorPath);
+      const editorPath = generateEditorPath(...paths);
+      await router.push(editorPath);
+      mutateEditorMode(EditorMode.Editor);
 
       // close modal
       closeCreateModal();

+ 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 { IRevisionHasPageId } from '@growi/core';
+
 import { useCurrentPageId } from '~/stores/context';
 import { useSWRxPageRevisions } from '~/stores/page';
 import loggerFactory from '~/utils/logger';
 
-import PageRevisionTable from './PageHistory/PageRevisionTable';
+import { PageRevisionTable } from './PageHistory/PageRevisionTable';
 import PaginationWrapper from './PaginationWrapper';
-import RevisionComparer from './RevisionComparer/RevisionComparer';
+import { RevisionComparer } from './RevisionComparer/RevisionComparer';
 
 const logger = loggerFactory('growi:PageHistory');
 
-const PageHistory = () => {
+export const PageHistory = (): JSX.Element => {
+
   const [activePage, setActivePage] = useState(1);
+
   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(() => {
     if (revisionsData != null) {
@@ -24,17 +30,17 @@ const PageHistory = () => {
     }
   }, [revisionsData]);
 
-
   const pagingLimit = 10;
 
-  if (revisionsData == null) {
+  if (revisionsData == null || sourceRevision == null || targetRevision == null || currentPageId == null) {
     return (
       <div className="text-muted text-center">
         <i className="fa fa-2x fa-spinner fa-pulse mt-3"></i>
       </div>
     );
   }
-  function pager() {
+
+  const pager = () => {
     return (
       <PaginationWrapper
         activePage={activePage}
@@ -44,7 +50,7 @@ const PageHistory = () => {
         align="center"
       />
     );
-  }
+  };
 
   return (
     <div className="revision-history" data-testid="page-history">
@@ -67,5 +73,3 @@ const PageHistory = () => {
     </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 { IRevisionHasId } from '@growi/core';
 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 handleCompareLatestRevisionButton = () => {
@@ -35,7 +47,6 @@ class PageRevisionTable extends React.Component {
         <td className="col" key={`revision-history-top-${revisionId}`}>
           <div className="d-lg-flex">
             <Revision
-              t={this.props.t}
               revision={revision}
               isLatestRevision={revision === latestRevision}
               hasDiff={hasDiff}
@@ -98,73 +109,38 @@ class PageRevisionTable extends React.Component {
         </td>
       </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>
+  );
+
+};

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

@@ -7,6 +7,8 @@ import { useIsNotFound } from '~/stores/context';
 
 import LinkedPagePath from '../models/linked-page-path';
 
+import PagePathHierarchicalLink from './PagePathHierarchicalLink';
+
 const { isTrashPage } = pagePathUtils;
 
 type Props = {
@@ -17,7 +19,6 @@ type Props = {
 }
 
 const CopyDropdown = dynamic(() => import('./Page/CopyDropdown'), { ssr: false });
-const PagePathHierarchicalLink = dynamic(() => import('./PagePathHierarchicalLink'), { ssr: false });
 
 const PagePathNav: FC<Props> = (props: Props) => {
   const {
@@ -51,7 +52,7 @@ const PagePathNav: FC<Props> = (props: Props) => {
   return (
     <div className="grw-page-path-nav">
       {formerLink}
-      <span className="d-flex align-items-center">
+      <div className="d-flex align-items-center">
         <h1 className="m-0">{latterLink}</h1>
         { pageId != null && !isNotFound && (
           <div className="mx-2">
@@ -60,7 +61,7 @@ const PagePathNav: FC<Props> = (props: Props) => {
             </CopyDropdown>
           </div>
         ) }
-      </span>
+      </div>
     </div>
   );
 };

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

@@ -18,7 +18,7 @@ import { useCurrentUser } from '~/stores/context';
 import {
   ILegacyPrivatePage, usePrivateLegacyPagesMigrationModal,
 } from '~/stores/modal';
-import { useSWRxV5MigrationStatus } from '~/stores/page-listing';
+import { usePageTreeTermManager, useSWRxV5MigrationStatus } from '~/stores/page-listing';
 import {
   useSWRxSearch,
 } from '~/stores/search';
@@ -213,6 +213,7 @@ const PrivateLegacyPages = (): JSX.Element => {
   });
 
   const { data: migrationStatus, mutate: mutateMigrationStatus } = useSWRxV5MigrationStatus();
+  const { advance: advancePt } = usePageTreeTermManager();
 
   const searchInvokedHandler = useCallback((_keyword: string) => {
     mutateMigrationStatus();
@@ -314,6 +315,7 @@ const PrivateLegacyPages = (): JSX.Element => {
         closeModal();
         mutateMigrationStatus();
         mutate();
+        advancePt();
       },
     );
   }, [data, mutate, openModal, closeModal, mutateMigrationStatus]);

+ 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 PropTypes from 'prop-types';
 import { CopyToClipboard } from 'react-copy-to-clipboard';
 import {
   Dropdown, DropdownToggle, DropdownMenu, DropdownItem,
@@ -10,36 +9,40 @@ import {
 
 import { useCurrentPagePath } from '~/stores/context';
 
-import RevisionDiff from '../PageHistory/RevisionDiff';
+import { RevisionDiff } from '../PageHistory/RevisionDiff';
 
+import styles from './RevisionComparer.module.scss';
 
 const { encodeSpaces } = pagePathUtils;
 
-/* eslint-disable react/prop-types */
 const DropdownItemContents = ({ title, contents }) => (
   <>
     <div className="h6 mt-1 mb-2"><strong>{title}</strong></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 { data: currentPagePath } = useCurrentPagePath();
-  const [dropdownOpen, setDropdownOpen] = useState(false);
+
   const {
-    sourceRevision, targetRevision,
-    currentPageId,
+    sourceRevision, targetRevision, currentPageId,
   } = props;
 
-  function toggleDropdown() {
+  const { data: currentPagePath } = useCurrentPagePath();
+  const [dropdownOpen, setDropdownOpen] = useState(false);
+
+  const toggleDropdown = () => {
     setDropdownOpen(!dropdownOpen);
-  }
+  };
 
-  const generateURL = (pathName) => {
+  const generateURL = (pathName: string) => {
     const { origin } = window.location;
 
     const url = new URL(pathName, origin);
@@ -49,24 +52,17 @@ const RevisionComparer = (props) => {
       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) {
     return <>{ t('not_found_page.page_not_exist')}</>;
   }
 
   return (
-    <div className="revision-compare">
+    <div className={`${styles['revision-compare']} revision-compare`}>
       <div className="d-flex">
         <h4 className="align-self-center">{ t('page_history.comparing_revisions') }</h4>
         <Dropdown
@@ -115,11 +111,3 @@ const RevisionComparer = (props) => {
     </div>
   );
 };
-
-RevisionComparer.propTypes = {
-  sourceRevision: PropTypes.instanceOf(Object),
-  targetRevision: PropTypes.instanceOf(Object),
-  currentPageId: PropTypes.string,
-};
-
-export default RevisionComparer;

+ 4 - 0
packages/app/src/interfaces/customize.ts

@@ -19,3 +19,7 @@ export type IHighlightJsCssSelectorOptions = {
     border: boolean
   }
 }
+
+export type IResLayoutSetting = {
+  isContainerFluid: boolean,
+};

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

+ 20 - 8
packages/app/src/pages/[[...path]].page.tsx

@@ -35,7 +35,8 @@ import { IUserUISettings } from '~/interfaces/user-ui-settings';
 import { PageModel, PageDocument } from '~/server/models/page';
 import { PageRedirectModel } from '~/server/models/page-redirect';
 import { UserUISettingsModel } from '~/server/models/user-ui-settings';
-import { useSWRxCurrentPage, useSWRxIsGrantNormalized } from '~/stores/page';
+import { useSWRxLayoutSetting } from '~/stores/admin/customize';
+import { useSWRxCurrentPage, useSWRxIsGrantNormalized, useSWRxPageInfo } from '~/stores/page';
 import { useRedirectFrom } from '~/stores/page-redirect';
 import {
   usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser, useSidebarCollapsed, useCurrentSidebarContents, useCurrentProductNavWidth, useSelectedGrant,
@@ -62,12 +63,13 @@ import {
   useIsAclEnabled, useIsUserPage, useIsSearchPage,
   useCsrfToken, useIsSearchScopeChildrenAsDefault, useCurrentPageId, useCurrentPathname,
   useIsSlackConfigured, useRendererConfig, useEditingMarkdown,
-  useEditorConfig, useIsAllReplyShown, useIsUploadableFile, useIsUploadableImage,
+  useEditorConfig, useIsAllReplyShown, useIsUploadableFile, useIsUploadableImage, useLayoutSetting,
 } from '../stores/context';
 
 import {
   CommonProps, getNextI18NextConfig, getServerSideCommonProps, useCustomTitle,
 } from './utils/commons';
+import { calcIsContainerFluid } from './utils/layout';
 // import { useCurrentPageSWR } from '../stores/page';
 
 
@@ -152,7 +154,7 @@ type Props = CommonProps & {
   noCdn: string,
   // highlightJsStyle: string,
   isAllReplyShown: boolean,
-  // isContainerFluid: boolean,
+  isContainerFluid: boolean,
   editorConfig: EditorConfig,
   isEnabledStaleNotification: boolean,
   // isEnabledLinebreaks: boolean,
@@ -231,9 +233,6 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
 
   const { pageWithMeta, userUISettings } = props;
 
-  useSWRxCurrentPage(undefined, pageWithMeta?.data ?? null); // store initial data
-  useEditingMarkdown(pageWithMeta?.data.revision?.body ?? '');
-
   const pageId = pageWithMeta?.data._id;
   const pagePath = pageWithMeta?.data.path ?? (!_isPermalink(props.currentPathname) ? props.currentPathname : undefined);
 
@@ -244,6 +243,12 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
   useCurrentPathname(props.currentPathname);
   useIsTrashPage(pagePath != null && _isTrashPage(pagePath));
 
+  useSWRxCurrentPage(undefined, pageWithMeta?.data ?? null); // store initial data
+  useEditingMarkdown(pageWithMeta?.data.revision?.body ?? '');
+
+  const { data: layoutSetting } = useLayoutSetting({ isContainerFluid: props.isContainerFluid });
+  const { data: dataPageInfo } = useSWRxPageInfo(pageId);
+
   const { data: grantData } = useSWRxIsGrantNormalized(pageId);
   const { mutate: mutateSelectedGrant } = useSelectedGrant();
 
@@ -279,6 +284,13 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
 
   const isTopPagePath = isTopPage(pageWithMeta?.data.path ?? '');
 
+  const isContainerFluidEachPage = dataPageInfo == null || !('expandContentWidth' in dataPageInfo)
+    ? null
+    : dataPageInfo.expandContentWidth;
+  const isContainerFluidDefault = props.isContainerFluid;
+  const isContainerFluidAdmin = layoutSetting?.isContainerFluid;
+  const isContainerFluid = calcIsContainerFluid(isContainerFluidEachPage, isContainerFluidDefault, isContainerFluidAdmin);
+
   return (
     <>
       <Head>
@@ -289,7 +301,7 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
         */}
       </Head>
       {/* <BasicLayout title={useCustomTitle(props, t('GROWI'))} className={classNames.join(' ')}> */}
-      <BasicLayout title={useCustomTitle(props, 'GROWI')} className={classNames.join(' ')} expandContainer={props.isContainerFluid}>
+      <BasicLayout title={useCustomTitle(props, 'GROWI')} className={classNames.join(' ')} expandContainer={isContainerFluid}>
         <div className="h-100 d-flex flex-column justify-content-between">
           <header className="py-0 position-relative">
             <GrowiContextualSubNavigation isLinkSharingDisabled={props.disableLinkSharing} />
@@ -496,7 +508,7 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
   props.noCdn = configManager.getConfig('crowi', 'app:noCdn');
   // props.highlightJsStyle = configManager.getConfig('crowi', 'customize:highlightJsStyle');
   props.isAllReplyShown = configManager.getConfig('crowi', 'customize:isAllReplyShown');
-  // props.isContainerFluid = configManager.getConfig('crowi', 'customize:isContainerFluid');
+  props.isContainerFluid = configManager.getConfig('crowi', 'customize:isContainerFluid');
   props.isEnabledStaleNotification = configManager.getConfig('crowi', 'customize:isEnabledStaleNotification');
   // props.isEnabledLinebreaks = configManager.getConfig('markdown', 'markdown:isEnabledLinebreaks');
   // props.isEnabledLinebreaksInComments = configManager.getConfig('markdown', 'markdown:isEnabledLinebreaksInComments');

+ 28 - 14
packages/app/src/pages/_app.page.tsx

@@ -1,29 +1,40 @@
 import React, { useEffect } from 'react';
 
+import { isServer } from '@growi/core';
 import { appWithTranslation } from 'next-i18next';
 import { AppProps } from 'next/app';
 import { DndProvider } from 'react-dnd';
 import { HTML5Backend } from 'react-dnd-html5-backend';
-
-import '~/styles/style-next.scss';
-import '~/styles/style-themes.scss';
-// import InterceptorManager from '~/service/interceptor-manager';
+import { SWRConfig } from 'swr';
 
 import * as nextI18nConfig from '^/config/next-i18next.config';
 
-import { NextThemesProvider } from '~/stores/use-next-themes';
-
-import { useI18nextHMR } from '../services/i18next-hmr';
+import { useI18nextHMR } from '~/services/i18next-hmr';
 import {
   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 { CommonProps } from './utils/commons';
 import { registerTransformerForObjectId } from './utils/objectid-transformer';
-// import { useInterceptorManager } from '~/stores/interceptor';
+
+import '~/styles/style-next.scss';
+import '~/styles/style-themes.scss';
+
 
 const isDev = process.env.NODE_ENV === 'development';
 
+const swrConfig: SWRConfigValue = {
+  ...swrGlobalConfiguration,
+  // set the request scoped cache provider in server
+  provider: isServer()
+    ? cache => new Map(cache)
+    : undefined,
+};
+
+
 type GrowiAppProps = AppProps & {
   pageProps: CommonProps;
 };
@@ -37,6 +48,7 @@ function GrowiApp({ Component, pageProps }: GrowiAppProps): JSX.Element {
     import('bootstrap/dist/js/bootstrap');
   }, []);
 
+
   const commonPageProps = pageProps as CommonProps;
   // useInterceptorManager(new InterceptorManager());
   useAppTitle(commonPageProps.appTitle);
@@ -46,11 +58,13 @@ function GrowiApp({ Component, pageProps }: GrowiAppProps): JSX.Element {
   useGrowiVersion(commonPageProps.growiVersion);
 
   return (
-    <NextThemesProvider>
-      <DndProvider backend={HTML5Backend}>
-        <Component {...pageProps} />
-      </DndProvider>
-    </NextThemesProvider>
+    <SWRConfig value={swrConfig}>
+      <NextThemesProvider>
+        <DndProvider backend={HTML5Backend}>
+          <Component {...pageProps} />
+        </DndProvider>
+      </NextThemesProvider>
+    </SWRConfig>
   );
 }
 

+ 12 - 0
packages/app/src/pages/utils/layout.ts

@@ -0,0 +1,12 @@
+// Use `props.isContainerFluid` as default, `layoutSetting.isContainerFluid` as admin setting, `dataPageInfo.expandContentWidth` as each page's setting
+export const calcIsContainerFluid = (
+    isContainerFluidEachPage: boolean | undefined | null,
+    isContainerFluidDefault: boolean,
+    isContainerFluidAdmin: boolean | undefined,
+): boolean => {
+  const isContainerFluid = isContainerFluidEachPage == null
+    ? isContainerFluidAdmin ?? isContainerFluidDefault
+    : isContainerFluidEachPage ?? isContainerFluidAdmin ?? isContainerFluidDefault;
+
+  return isContainerFluid;
+};

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

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

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

@@ -49,8 +49,6 @@ const multer = require('multer');
  *        properties:
  *          isEnabledTimeline:
  *            type: boolean
- *          isSavedStatesOfTabChanges:
- *            type: boolean
  *          isEnabledAttachTitleHeader:
  *            type: boolean
  *          pageLimitationS:
@@ -122,7 +120,6 @@ module.exports = (crowi) => {
     ],
     function: [
       body('isEnabledTimeline').isBoolean(),
-      body('isSavedStatesOfTabChanges').isBoolean(),
       body('isEnabledAttachTitleHeader').isBoolean(),
       body('pageLimitationS').isInt().isInt({ min: 1, max: 1000 }),
       body('pageLimitationM').isInt().isInt({ min: 1, max: 1000 }),
@@ -180,7 +177,6 @@ module.exports = (crowi) => {
     const customizeParams = {
       themeType: await crowi.configManager.getConfig('crowi', 'customize:theme'),
       isEnabledTimeline: await crowi.configManager.getConfig('crowi', 'customize:isEnabledTimeline'),
-      isSavedStatesOfTabChanges: await crowi.configManager.getConfig('crowi', 'customize:isSavedStatesOfTabChanges'),
       isEnabledAttachTitleHeader: await crowi.configManager.getConfig('crowi', 'customize:isEnabledAttachTitleHeader'),
       pageLimitationS: await crowi.configManager.getConfig('crowi', 'customize:showPageLimitationS'),
       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) => {
     const requestParams = {
       'customize:isEnabledTimeline': req.body.isEnabledTimeline,
-      'customize:isSavedStatesOfTabChanges': req.body.isSavedStatesOfTabChanges,
       'customize:isEnabledAttachTitleHeader': req.body.isEnabledAttachTitleHeader,
       'customize:showPageLimitationS': req.body.pageLimitationS,
       'customize:showPageLimitationM': req.body.pageLimitationM,
@@ -437,7 +432,6 @@ module.exports = (crowi) => {
       await crowi.configManager.updateConfigsInTheSameNamespace('crowi', requestParams);
       const customizedParams = {
         isEnabledTimeline: await crowi.configManager.getConfig('crowi', 'customize:isEnabledTimeline'),
-        isSavedStatesOfTabChanges: await crowi.configManager.getConfig('crowi', 'customize:isSavedStatesOfTabChanges'),
         isEnabledAttachTitleHeader: await crowi.configManager.getConfig('crowi', 'customize:isEnabledAttachTitleHeader'),
         pageLimitationS: await crowi.configManager.getConfig('crowi', 'customize:showPageLimitationS'),
         pageLimitationM: await crowi.configManager.getConfig('crowi', 'customize:showPageLimitationM'),

+ 1 - 1
packages/app/src/server/service/page.ts

@@ -2484,7 +2484,7 @@ class PageService {
       throw Error(`The maximum number of pageIds allowed is ${LIMIT_FOR_MULTIPLE_PAGE_OP}.`);
     }
 
-    this.normalizeParentRecursivelyByPages(pages, user);
+    await this.normalizeParentRecursivelyByPages(pages, user);
 
     return;
   }

+ 27 - 0
packages/app/src/stores/admin/customize.tsx

@@ -0,0 +1,27 @@
+import { useCallback } from 'react';
+
+import useSWR, { SWRResponse } from 'swr';
+
+import { apiv3Get } from '~/client/util/apiv3-client';
+import { IResLayoutSetting } from '~/interfaces/customize';
+
+import { useLayoutSetting } from '../context';
+
+
+export const useSWRxLayoutSetting = (fallbackData?: IResLayoutSetting): SWRResponse<IResLayoutSetting, Error> => {
+  const { mutate: mutateStatic } = useLayoutSetting();
+
+  const fetcher = useCallback(async() => {
+    const res = await apiv3Get('/customize-setting/layout');
+
+    mutateStatic(res.data);
+
+    return res.data;
+  }, [mutateStatic]);
+
+  return useSWR(
+    '/customize-setting/layout',
+    fetcher,
+    { fallbackData },
+  );
+};

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

@@ -5,6 +5,7 @@ import useSWRImmutable from 'swr/immutable';
 
 
 import { SupportedActionType } from '~/interfaces/activity';
+import { IResLayoutSetting } from '~/interfaces/customize';
 import { EditorConfig } from '~/interfaces/editor-settings';
 // import { CustomWindow } from '~/interfaces/global';
 import { RendererConfig } from '~/interfaces/services/renderer';
@@ -258,6 +259,10 @@ export const useShowPageLimitationXL = (initialData?: number): SWRResponse<numbe
   return useStaticSWR('showPageLimitationXL', initialData);
 };
 
+export const useLayoutSetting = (initialData?: IResLayoutSetting): SWRResponse<IResLayoutSetting, Error> => {
+  return useStaticSWR('layoutSetting', initialData);
+};
+
 export const useCustomizeTitle = (initialData?: string): SWRResponse<string, Error> => {
   return useStaticSWR('CustomizeTitle', initialData);
 };

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

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

+ 9 - 5
packages/app/src/stores/use-static-swr.tsx

@@ -1,6 +1,7 @@
 import assert from 'assert';
+
 import {
-  Key, SWRConfiguration, SWRResponse,
+  Key, SWRConfiguration, SWRResponse, useSWRConfig,
 } from 'swr';
 import useSWRImmutable from 'swr/immutable';
 
@@ -19,12 +20,15 @@ export function useStaticSWR<Data, Error>(
 
   assert.notStrictEqual(configuration?.fetcher, null, 'useStaticSWR does not support \'configuration.fetcher\'');
 
-  const swrResponse = useSWRImmutable(key, null, configuration);
+  const { cache } = useSWRConfig();
+  const swrResponse = useSWRImmutable(key, null, {
+    ...configuration,
+    fallbackData: configuration?.fallbackData ?? cache.get(key),
+  });
 
-  // mutate
+  // write data to cache directly
   if (data !== undefined) {
-    const { mutate } = swrResponse;
-    mutate(data);
+    cache.set(key, data);
   }
 
   return swrResponse;

+ 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

+ 6 - 2
packages/app/src/utils/swr-utils.ts

@@ -1,5 +1,9 @@
-import { SWRConfiguration } from 'swr';
+import { ProviderConfiguration, PublicConfiguration } from 'swr/dist/types';
 
-export const swrGlobalConfiguration: SWRConfiguration = {
+export type SWRConfigValue = Partial<PublicConfiguration> & Partial<ProviderConfiguration> & {
+  provider?: (cache) => any | undefined,
+};
+
+export const swrGlobalConfiguration: SWRConfigValue = {
   errorRetryCount: 1,
 };

+ 1 - 0
packages/core/src/interfaces/page.ts

@@ -31,6 +31,7 @@ export type IPage = {
   deleteUser: Ref<IUser>,
   deletedAt: Date,
   latestRevision?: Ref<IRevision>,
+  expandContentWidth?: boolean,
 }
 
 export type IPagePopulatedToList = Omit<IPageHasId, 'lastUpdateUser'> & {

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

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