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

Merge branch 'master' into feat/tsfc-RevisionDiff

jam411 3 лет назад
Родитель
Сommit
ab5ccfb849
26 измененных файлов с 289 добавлено и 273 удалено
  1. 1 2
      .github/workflows/ci-app-prod.yml
  2. 2 2
      .github/workflows/reusable-app-prod.yml
  3. 1 1
      packages/app/src/components/Admin/Customize/Customize.jsx
  4. 0 96
      packages/app/src/components/Admin/Customize/CustomizeTitle.jsx
  5. 79 0
      packages/app/src/components/Admin/Customize/CustomizeTitle.tsx
  6. 4 3
      packages/app/src/components/Layout/AdminLayout.tsx
  7. 1 1
      packages/app/src/components/Layout/SearchResultLayout.tsx
  8. 0 138
      packages/app/src/components/Page/RevisionLoader.jsx
  9. 118 0
      packages/app/src/components/Page/RevisionLoader.tsx
  10. 0 6
      packages/app/src/components/PageComment/CommentEditor.module.scss
  11. 1 6
      packages/app/src/components/PageComment/CommentEditor.tsx
  12. 1 1
      packages/app/src/components/PageTimeline.tsx
  13. 2 4
      packages/app/src/components/ReactMarkdownComponents/CodeBlock.module.scss
  14. 3 1
      packages/app/src/components/SearchPage/SearchResultContent.tsx
  15. 2 1
      packages/app/src/pages/[[...path]].page.tsx
  16. 17 0
      packages/app/src/pages/_error.page.tsx
  17. 8 3
      packages/app/src/pages/admin/[[...path]].page.tsx
  18. 3 1
      packages/app/src/pages/me/[[...path]].page.tsx
  19. 2 1
      packages/app/src/pages/share/[[...path]].page.tsx
  20. 2 1
      packages/app/src/pages/tags.page.tsx
  21. 2 1
      packages/app/src/pages/trash.page.tsx
  22. 4 0
      packages/app/src/stores/context.tsx
  23. 12 0
      packages/app/src/styles/atoms/_code.scss
  24. 15 0
      packages/app/src/styles/atoms/_mixins.scss
  25. 1 0
      packages/app/src/styles/style-next.scss
  26. 8 4
      packages/app/src/styles/theme/_apply-colors.scss

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

@@ -52,8 +52,7 @@ jobs:
     uses: weseek/growi/.github/workflows/reusable-app-prod.yml@master
     uses: weseek/growi/.github/workflows/reusable-app-prod.yml@master
     with:
     with:
       node-version: 16.x
       node-version: 16.x
-      # skip-cypress: ${{ contains( github.event.pull_request.labels.*.name, 'dependencies' ) && contains( github.event.pull_request.labels.*.name, 'github_actions' ) }}
-      skip-cypress: true
+      skip-cypress: ${{ contains( github.event.pull_request.labels.*.name, 'dependencies' ) && contains( github.event.pull_request.labels.*.name, 'github_actions' ) }}
       cypress-report-artifact-name: Cypress report
       cypress-report-artifact-name: Cypress report
     secrets:
     secrets:
       SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
       SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

+ 2 - 2
.github/workflows/reusable-app-prod.yml

@@ -215,7 +215,7 @@ jobs:
 
 
     - uses: actions/setup-node@v3
     - uses: actions/setup-node@v3
       with:
       with:
-        node-version: ${{ matrix.node-version }}
+        node-version: ${{ inputs.node-version }}
         cache: 'yarn'
         cache: 'yarn'
         cache-dependency-path: '**/yarn.lock'
         cache-dependency-path: '**/yarn.lock'
 
 
@@ -237,7 +237,7 @@ jobs:
     - name: Download production files artifact
     - name: Download production files artifact
       uses: actions/download-artifact@v3
       uses: actions/download-artifact@v3
       with:
       with:
-        name: Production Files
+        name: Production Files (node${{ inputs.node-version }})
 
 
     - name: Extract procution files artifact
     - name: Extract procution files artifact
       run: |
       run: |

+ 1 - 1
packages/app/src/components/Admin/Customize/Customize.jsx

@@ -19,7 +19,7 @@ import CustomizeLogoSetting from './CustomizeLogoSetting';
 import CustomizeScriptSetting from './CustomizeScriptSetting';
 import CustomizeScriptSetting from './CustomizeScriptSetting';
 import CustomizeSidebarSetting from './CustomizeSidebarSetting';
 import CustomizeSidebarSetting from './CustomizeSidebarSetting';
 import CustomizeThemeSetting from './CustomizeThemeSetting';
 import CustomizeThemeSetting from './CustomizeThemeSetting';
-import CustomizeTitle from './CustomizeTitle';
+import { CustomizeTitle } from './CustomizeTitle';
 
 
 const logger = loggerFactory('growi:services:AdminCustomizePage');
 const logger = loggerFactory('growi:services:AdminCustomizePage');
 
 

+ 0 - 96
packages/app/src/components/Admin/Customize/CustomizeTitle.jsx

@@ -1,96 +0,0 @@
-/* eslint-disable max-len */
-import React from 'react';
-
-import { withTranslation } from 'next-i18next';
-import PropTypes from 'prop-types';
-import { Card, CardBody } from 'reactstrap';
-
-import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
-import { toastSuccess, toastError } from '~/client/util/apiNotification';
-
-import { withUnstatedContainers } from '../../UnstatedUtils';
-import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
-
-class CustomizeTitle extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.onClickSubmit = this.onClickSubmit.bind(this);
-  }
-
-  async onClickSubmit() {
-    const { t, adminCustomizeContainer } = this.props;
-
-    try {
-      await adminCustomizeContainer.updateCustomizeTitle();
-      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_setting.custom_title') }));
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }
-
-  render() {
-    const { t, adminCustomizeContainer } = this.props;
-    const { currentCustomizeTitle } = adminCustomizeContainer.state;
-
-    return (
-      <React.Fragment>
-        <div className="row">
-          <div className="col-12">
-            <h2 className="admin-setting-header">{t('admin:customize_setting.custom_title')}</h2>
-          </div>
-
-          <div className="col-12">
-            <Card className="card well">
-              <CardBody className="px-0 py-2">
-                {/* eslint-disable react/no-danger */}
-                <p dangerouslySetInnerHTML={{ __html: t('admin:customize_setting.custom_title_detail') }} />
-                <ul>
-                  <li>
-                    <span dangerouslySetInnerHTML={{ __html: t('admin:customize_setting.custom_title_detail_placeholder1') }} />
-                  </li>
-                  <li>
-                    <span dangerouslySetInnerHTML={{ __html: t('admin:customize_setting.custom_title_detail_placeholder2') }} />
-                  </li>
-                  <li>
-                    <span dangerouslySetInnerHTML={{ __html: t('admin:customize_setting.custom_title_detail_placeholder3') }} />
-                  </li>
-                </ul>
-                {/* eslint-enable react/no-danger */}
-              </CardBody>
-            </Card>
-          </div>
-
-          {/* TODO i18n */}
-          <div className="form-text text-muted col-12">
-            Default Value: <code>&#123;&#123;pagename&#125;&#125; - &#123;&#123;sitename&#125;&#125;</code>
-            <br />
-            Default Output Example: <code className="xml">&lt;title&gt;Page name - My GROWI&lt;&#047;title&gt;</code>
-          </div>
-          <div className="form-group col-12">
-            <input
-              className="form-control"
-              defaultValue={currentCustomizeTitle}
-              onChange={(e) => { adminCustomizeContainer.changeCustomizeTitle(e.target.value) }}
-            />
-          </div>
-          <div className="col-12">
-            <AdminUpdateButtonRow onClick={this.onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
-          </div>
-        </div>
-      </React.Fragment>
-    );
-  }
-
-}
-
-const CustomizeTitleWrapper = withUnstatedContainers(CustomizeTitle, [AdminCustomizeContainer]);
-
-CustomizeTitle.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  adminCustomizeContainer: PropTypes.instanceOf(AdminCustomizeContainer).isRequired,
-};
-
-export default withTranslation()(CustomizeTitleWrapper);

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

@@ -0,0 +1,79 @@
+import React, { FC, useState } from 'react';
+
+import { useTranslation } from 'next-i18next';
+import { Card, CardBody } from 'reactstrap';
+
+import { toastSuccess, toastError } from '~/client/util/apiNotification';
+import { apiv3Put } from '~/client/util/apiv3-client';
+import { useCustomizeTitle } from '~/stores/context';
+
+import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
+
+export const CustomizeTitle: FC = () => {
+
+  const { t } = useTranslation('admin');
+
+  const { data: customizeTitle } = useCustomizeTitle();
+
+  const [currentCustomizeTitle, setCrrentCustomizeTitle] = useState(customizeTitle);
+
+  const onClickSubmit = async() => {
+    try {
+      await apiv3Put('/customize-setting/customize-title', {
+        customizeTitle: currentCustomizeTitle,
+      });
+      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_setting.custom_title') }));
+    }
+    catch (err) {
+      toastError(err);
+    }
+  };
+
+  return (
+    <React.Fragment>
+      <div className="row">
+        <div className="col-12">
+          <h2 className="admin-setting-header">{t('admin:customize_settings.custom_title')}</h2>
+        </div>
+
+        <div className="col-12">
+          <Card className="card well">
+            <CardBody className="px-0 py-2">
+              {/* eslint-disable react/no-danger */}
+              <p dangerouslySetInnerHTML={{ __html: t('admin:customize_settings.custom_title_detail') }} />
+              <ul>
+                <li>
+                  <span dangerouslySetInnerHTML={{ __html: t('admin:customize_settings.custom_title_detail_placeholder1') }} />
+                </li>
+                <li>
+                  <span dangerouslySetInnerHTML={{ __html: t('admin:customize_settings.custom_title_detail_placeholder2') }} />
+                </li>
+                <li>
+                  <span dangerouslySetInnerHTML={{ __html: t('admin:customize_settings.custom_title_detail_placeholder3') }} />
+                </li>
+              </ul>
+              {/* eslint-enable react/no-danger */}
+            </CardBody>
+          </Card>
+        </div>
+
+        {/* TODO i18n */}
+        <div className="form-text text-muted col-12">
+            Default Value: <code>&#123;&#123;pagename&#125;&#125; - &#123;&#123;sitename&#125;&#125;</code>
+          <br />
+            Default Output Example: <code className="xml">&lt;title&gt;Page name - My GROWI&lt;&#047;title&gt;</code>
+        </div>
+        <div className="form-group col-12">
+          <input
+            className="form-control"
+            defaultValue={currentCustomizeTitle}
+            onChange={(e) => { setCrrentCustomizeTitle(e.target.value) }}
+          />
+        </div>
+        <div className="col-12">
+          <AdminUpdateButtonRow onClick={onClickSubmit} disabled={false} />
+        </div>
+      </div>
+    </React.Fragment>
+  );
+};

+ 4 - 3
packages/app/src/components/Layout/AdminLayout.tsx

@@ -13,6 +13,7 @@ const AdminNotFoundPage = dynamic(() => import('../Admin/NotFoundPage').then(mod
 
 
 type Props = {
 type Props = {
   title: string
   title: string
+  componentTitle: string
   /**
   /**
    * Set the current option of AdminNavigation
    * Set the current option of AdminNavigation
    * Expected it is in ["home", "app", "security", "markdown", "customize", "importer", "export",
    * Expected it is in ["home", "app", "security", "markdown", "customize", "importer", "export",
@@ -24,7 +25,7 @@ type Props = {
 
 
 
 
 const AdminLayout = ({
 const AdminLayout = ({
-  children, title, selectedNavOpt,
+  children, title, selectedNavOpt, componentTitle,
 }: Props): JSX.Element => {
 }: Props): JSX.Element => {
 
 
   const AdminNavigation = dynamic(() => import('~/components/Admin/Common/AdminNavigation'), { ssr: false });
   const AdminNavigation = dynamic(() => import('~/components/Admin/Common/AdminNavigation'), { ssr: false });
@@ -35,8 +36,8 @@ const AdminLayout = ({
       <div className={`admin-page ${styles['admin-page']}`}>
       <div className={`admin-page ${styles['admin-page']}`}>
         <GrowiNavbar />
         <GrowiNavbar />
 
 
-        <header className="py-0 position-relative">
-          <h1 className="title px-3">{title}</h1>
+        <header className="py-0 container-fluid">
+          <h1 className="title px-3">{componentTitle}</h1>
         </header>
         </header>
         <div id="main" className="main">
         <div id="main" className="main">
           <div className="container-fluid">
           <div className="container-fluid">

+ 1 - 1
packages/app/src/components/Layout/SearchResultLayout.tsx

@@ -20,7 +20,7 @@ const SearchResultLayout = ({
   }
   }
 
 
   return (
   return (
-    <div className={`${commonStyles['on-search']}`}>
+    <div className={`on-search ${commonStyles['on-search']}`}>
       <BasicLayout title={title} className={classNames.join(' ')}>
       <BasicLayout title={title} className={classNames.join(' ')}>
         <div id="grw-fav-sticky-trigger" className="sticky-top"></div>
         <div id="grw-fav-sticky-trigger" className="sticky-top"></div>
         <div id="main" className="main search-page mt-0">
         <div id="main" className="main search-page mt-0">

+ 0 - 138
packages/app/src/components/Page/RevisionLoader.jsx

@@ -1,138 +0,0 @@
-import React from 'react';
-
-import { useTranslation } from 'next-i18next';
-import PropTypes from 'prop-types';
-import { Waypoint } from 'react-waypoint';
-
-import { apiv3Get } from '~/client/util/apiv3-client';
-import { RendererOptions } from '~/services/renderer/renderer';
-import loggerFactory from '~/utils/logger';
-
-import RevisionRenderer from './RevisionRenderer';
-
-
-/**
- * Load data from server and render RevisionBody component
- */
-class RevisionLoader extends React.Component {
-
-  constructor(props) {
-    super(props);
-    this.logger = loggerFactory('growi:Page:RevisionLoader');
-
-    this.state = {
-      markdown: null,
-      isLoading: false,
-      isLoaded: false,
-      errors: null,
-    };
-
-    this.loadData = this.loadData.bind(this);
-    this.onWaypointChange = this.onWaypointChange.bind(this);
-  }
-
-  UNSAFE_componentWillMount() {
-    if (!this.props.lazy) {
-      this.loadData();
-    }
-  }
-
-  async loadData() {
-    if (!this.state.isLoaded && !this.state.isLoading) {
-      this.setState({ isLoading: true });
-    }
-
-    const { pageId, revisionId } = this.props;
-
-
-    // load data with REST API
-    try {
-      const res = await apiv3Get(`/revisions/${revisionId}`, { pageId });
-
-      this.setState({
-        markdown: res.data?.revision?.body,
-        errors: null,
-      });
-
-      if (this.props.onRevisionLoaded != null) {
-        this.props.onRevisionLoaded(res.data.revision);
-      }
-    }
-    catch (errors) {
-      this.setState({ errors });
-    }
-    finally {
-      this.setState({ isLoaded: true, isLoading: false });
-    }
-
-  }
-
-  onWaypointChange(event) {
-    if (event.currentPosition === Waypoint.above || event.currentPosition === Waypoint.inside) {
-      this.loadData();
-    }
-  }
-
-  render() {
-    // ----- before load -----
-    if (this.props.lazy && !this.state.isLoaded) {
-      return (
-        <Waypoint onPositionChange={this.onWaypointChange} bottomOffset="-100px">
-          <div className="wiki"></div>
-        </Waypoint>
-      );
-    }
-
-    // ----- loading -----
-    if (this.state.isLoading) {
-      return (
-        <div className="wiki">
-          <div className="text-muted text-center">
-            <i className="fa fa-2x fa-spinner fa-pulse mr-1"></i>
-          </div>
-        </div>
-      );
-    }
-
-    // ----- after load -----
-    const isForbidden = this.state.errors != null && this.state.errors[0].code === 'forbidden-page';
-    let markdown = this.state.markdown;
-    if (isForbidden) {
-      markdown = `<i class="icon-exclamation p-1"></i>${this.props.t('not_allowed_to_see_this_page')}`;
-    }
-    else if (this.state.errors != null) {
-      const errorMessages = this.state.errors.map((error) => {
-        return `<i class="icon-exclamation p-1"></i><span class="text-muted"><em>${error.message}</em></span>`;
-      });
-      markdown = errorMessages.join('\n');
-    }
-
-    return (
-      <RevisionRenderer
-        rendererOptions={this.props.rendererOptions}
-        markdown={markdown}
-      />
-    );
-  }
-
-}
-
-
-RevisionLoader.propTypes = {
-  t: PropTypes.func.isRequired,
-
-  rendererOptions: PropTypes.instanceOf(RendererOptions).isRequired,
-  pageId: PropTypes.string.isRequired,
-  pagePath: PropTypes.string.isRequired,
-  revisionId: PropTypes.string.isRequired,
-  lazy: PropTypes.bool,
-  onRevisionLoaded: PropTypes.func,
-  highlightKeywords: PropTypes.arrayOf(PropTypes.string),
-};
-
-const RevisionLoaderWrapperFC = (props) => {
-  const { t } = useTranslation();
-  return <RevisionLoader t={t} {...props} />;
-};
-
-export default RevisionLoaderWrapperFC;

+ 118 - 0
packages/app/src/components/Page/RevisionLoader.tsx

@@ -0,0 +1,118 @@
+import React, { useEffect, useState, useCallback } from 'react';
+
+import { Ref, IRevision, IRevisionHasId } from '@growi/core';
+import { useTranslation } from 'next-i18next';
+import { Waypoint } from 'react-waypoint';
+
+import { apiv3Get } from '~/client/util/apiv3-client';
+import { RendererOptions } from '~/services/renderer/renderer';
+import loggerFactory from '~/utils/logger';
+
+import RevisionRenderer from './RevisionRenderer';
+
+export type RevisionLoaderProps = {
+  rendererOptions: RendererOptions,
+  pageId: string,
+  revisionId: Ref<IRevision>,
+  lazy?: boolean,
+  onRevisionLoaded?: (revision: IRevisionHasId) => void,
+
+  pagePath: string,
+  highlightKeywords?: string[],
+}
+
+const logger = loggerFactory('growi:Page:RevisionLoader');
+
+/**
+ * Load data from server and render RevisionBody component
+ */
+export const RevisionLoader = (props: RevisionLoaderProps): JSX.Element => {
+  const { t } = useTranslation();
+
+  const {
+    rendererOptions, pageId, revisionId, lazy, onRevisionLoaded,
+  } = props;
+
+  const [isLoading, setIsLoading] = useState<boolean>();
+  const [isLoaded, setIsLoaded] = useState<boolean>();
+  const [markdown, setMarkdown] = useState<string>('');
+  const [errors, setErrors] = useState<any | null>();
+
+  const loadData = useCallback(async() => {
+    if (!isLoaded && !isLoading) {
+      setIsLoading(true);
+    }
+
+    // load data with REST API
+    try {
+      const res = await apiv3Get(`/revisions/${revisionId}`, { pageId });
+
+      setMarkdown(res.data?.revision?.body);
+      setErrors(null);
+
+      if (onRevisionLoaded != null) {
+        onRevisionLoaded(res.data.revision);
+      }
+    }
+    catch (errors) {
+      setErrors(errors);
+    }
+    finally {
+      setIsLoaded(true);
+      setIsLoading(false);
+    }
+
+  }, [isLoaded, isLoading, onRevisionLoaded, pageId, revisionId]);
+
+  useEffect(() => {
+    if (!lazy) {
+      loadData();
+    }
+  }, [lazy, loadData]);
+
+  const onWaypointChange = (event) => {
+    if (event.currentPosition === Waypoint.above || event.currentPosition === Waypoint.inside) {
+      loadData();
+    }
+    return;
+  };
+
+  /* ----- before load ----- */
+  if (lazy && !isLoaded) {
+    return (
+      <Waypoint onPositionChange={onWaypointChange} bottomOffset="-100px">
+        <div className="wiki"></div>
+      </Waypoint>
+    );
+  }
+
+  /* ----- loading ----- */
+  if (isLoading) {
+    return (
+      <div className="wiki">
+        <div className="text-muted text-center">
+          <i className="fa fa-2x fa-spinner fa-pulse mr-1"></i>
+        </div>
+      </div>
+    );
+  }
+
+  /* ----- after load ----- */
+  const isForbidden = errors != null && errors[0].code === 'forbidden-page';
+  if (isForbidden) {
+    setMarkdown(`<i class="icon-exclamation p-1"></i>${t('not_allowed_to_see_this_page')}`);
+  }
+  else if (errors != null) {
+    const errorMessages = errors.map((error) => {
+      return `<i class="icon-exclamation p-1"></i><span class="text-muted"><em>${error.message}</em></span>`;
+    });
+    setMarkdown(errorMessages.join('\n'));
+  }
+
+  return (
+    <RevisionRenderer
+      rendererOptions={rendererOptions}
+      markdown={markdown}
+    />
+  );
+};

+ 0 - 6
packages/app/src/components/PageComment/CommentEditor.module.scss

@@ -30,10 +30,4 @@
       padding-top: 0.5em;
       padding-top: 0.5em;
     }
     }
   }
   }
-
-  .page-comment-editor-skelton {
-    height: comment-inheritance.$codemirror-default-height;
-    margin-top: page-editor-inheritance.$navbar-editor-height;
-    margin-bottom: bs.$line-height-base + bs.$btn-padding-y;
-  }
 }
 }

+ 1 - 6
packages/app/src/components/PageComment/CommentEditor.tsx

@@ -20,7 +20,7 @@ import { useSWRxSlackChannels, useIsSlackEnabled } from '~/stores/editor';
 
 
 import { CustomNavTab } from '../CustomNavigation/CustomNav';
 import { CustomNavTab } from '../CustomNavigation/CustomNav';
 import NotAvailableForGuest from '../NotAvailableForGuest';
 import NotAvailableForGuest from '../NotAvailableForGuest';
-import { Skelton } from '../Skelton';
+import Editor from '../PageEditor/Editor';
 
 
 
 
 import { CommentPreview } from './CommentPreview';
 import { CommentPreview } from './CommentPreview';
@@ -29,11 +29,6 @@ import styles from './CommentEditor.module.scss';
 
 
 
 
 const SlackNotification = dynamic(() => import('../SlackNotification').then(mod => mod.SlackNotification), { ssr: false });
 const SlackNotification = dynamic(() => import('../SlackNotification').then(mod => mod.SlackNotification), { ssr: false });
-const Editor = dynamic(() => import('../PageEditor/Editor'),
-  {
-    ssr: false,
-    loading: () => <Skelton additionalClass="grw-skelton page-comment-editor-skelton" />,
-  });
 
 
 
 
 const navTabMapping = {
 const navTabMapping = {

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

@@ -8,7 +8,7 @@ import { IPageHasId } from '~/interfaces/page';
 import { useCurrentPagePath } from '~/stores/context';
 import { useCurrentPagePath } from '~/stores/context';
 import { useTimelineOptions } from '~/stores/renderer';
 import { useTimelineOptions } from '~/stores/renderer';
 
 
-import RevisionLoader from './Page/RevisionLoader';
+import { RevisionLoader } from './Page/RevisionLoader';
 import PaginationWrapper from './PaginationWrapper';
 import PaginationWrapper from './PaginationWrapper';
 
 
 import styles from './PageTimeline.module.scss';
 import styles from './PageTimeline.module.scss';

+ 2 - 4
packages/app/src/components/ReactMarkdownComponents/CodeBlock.module.scss

@@ -1,11 +1,9 @@
 @use '~/styles/variables' as var;
 @use '~/styles/variables' as var;
 @use '~/styles/bootstrap/init' as bs;
 @use '~/styles/bootstrap/init' as bs;
+@use '~/styles/atoms/mixins' as atm;
 
 
 .code-inline {
 .code-inline {
-  padding: 2px 4px;
-  font-family: var.$font-family-monospace-not-strictly;
-  border: 1px solid;
-  border-radius: bs.$border-radius;
+  @include atm.code-inline;
 }
 }
 
 
 .code-highlighted-title {
 .code-highlighted-title {

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

@@ -24,12 +24,14 @@ import { useFullTextSearchTermManager } from '~/stores/search';
 import { AdditionalMenuItemsRendererProps, ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
 import { AdditionalMenuItemsRendererProps, ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
 import { GrowiSubNavigationProps } from '../Navbar/GrowiSubNavigation';
 import { GrowiSubNavigationProps } from '../Navbar/GrowiSubNavigation';
 import { SubNavButtonsProps } from '../Navbar/SubNavButtons';
 import { SubNavButtonsProps } from '../Navbar/SubNavButtons';
+import { RevisionLoaderProps } from '../Page/RevisionLoader';
 import { PageCommentProps } from '../PageComment';
 import { PageCommentProps } from '../PageComment';
 import { PageContentFooterProps } from '../PageContentFooter';
 import { PageContentFooterProps } from '../PageContentFooter';
 
 
+
 const GrowiSubNavigation = dynamic<GrowiSubNavigationProps>(() => import('../Navbar/GrowiSubNavigation').then(mod => mod.GrowiSubNavigation), { ssr: false });
 const GrowiSubNavigation = dynamic<GrowiSubNavigationProps>(() => import('../Navbar/GrowiSubNavigation').then(mod => mod.GrowiSubNavigation), { ssr: false });
 const SubNavButtons = dynamic<SubNavButtonsProps>(() => import('../Navbar/SubNavButtons').then(mod => mod.SubNavButtons), { ssr: false });
 const SubNavButtons = dynamic<SubNavButtonsProps>(() => import('../Navbar/SubNavButtons').then(mod => mod.SubNavButtons), { ssr: false });
-const RevisionLoader = dynamic(() => import('../Page/RevisionLoader'), { ssr: false });
+const RevisionLoader = dynamic<RevisionLoaderProps>(() => import('../Page/RevisionLoader').then(mod => mod.RevisionLoader), { ssr: false });
 const PageComment = dynamic<PageCommentProps>(() => import('../PageComment').then(mod => mod.PageComment), { ssr: false });
 const PageComment = dynamic<PageCommentProps>(() => import('../PageComment').then(mod => mod.PageComment), { ssr: false });
 const PageContentFooter = dynamic<PageContentFooterProps>(() => import('../PageContentFooter').then(mod => mod.PageContentFooter), { ssr: false });
 const PageContentFooter = dynamic<PageContentFooterProps>(() => import('../PageContentFooter').then(mod => mod.PageContentFooter), { ssr: false });
 
 

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

@@ -59,7 +59,7 @@ import {
   useIsEnabledStaleNotification, useIsIdenticalPath,
   useIsEnabledStaleNotification, useIsIdenticalPath,
   useIsSearchServiceConfigured, useIsSearchServiceReachable, useDisableLinkSharing,
   useIsSearchServiceConfigured, useIsSearchServiceReachable, useDisableLinkSharing,
   useDrawioUri, useHackmdUri,
   useDrawioUri, useHackmdUri,
-  useIsAclEnabled, useIsUserPage,
+  useIsAclEnabled, useIsUserPage, useIsSearchPage,
   useCsrfToken, useIsSearchScopeChildrenAsDefault, useCurrentPageId, useCurrentPathname,
   useCsrfToken, useIsSearchScopeChildrenAsDefault, useCurrentPageId, useCurrentPathname,
   useIsSlackConfigured, useRendererConfig, useEditingMarkdown,
   useIsSlackConfigured, useRendererConfig, useEditingMarkdown,
   useEditorConfig, useIsAllReplyShown, useIsUploadableFile, useIsUploadableImage,
   useEditorConfig, useIsAllReplyShown, useIsUploadableFile, useIsUploadableImage,
@@ -204,6 +204,7 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
   useIsIdenticalPath(false); // TODO: need to initialize from props
   useIsIdenticalPath(false); // TODO: need to initialize from props
   // useIsAbleToDeleteCompletely(props.isAbleToDeleteCompletely);
   // useIsAbleToDeleteCompletely(props.isAbleToDeleteCompletely);
   useIsEnabledStaleNotification(props.isEnabledStaleNotification);
   useIsEnabledStaleNotification(props.isEnabledStaleNotification);
+  useIsSearchPage(false);
 
 
   useIsSearchServiceConfigured(props.isSearchServiceConfigured);
   useIsSearchServiceConfigured(props.isSearchServiceConfigured);
   useIsSearchServiceReachable(props.isSearchServiceReachable);
   useIsSearchServiceReachable(props.isSearchServiceReachable);

+ 17 - 0
packages/app/src/pages/_error.page.tsx

@@ -0,0 +1,17 @@
+import { NextPageContext } from 'next';
+import Error, { ErrorProps } from 'next/error';
+
+export default function ErrorPage(props: ErrorProps): JSX.Element {
+  return <Error {...props} />;
+}
+
+// add getInitialProps to disable "https://nextjs.org/docs/messages/prerender-error"
+//   Error: Export encountered errors on following paths:
+//     /_error: /404
+//     /_error: /500
+// see: https://github.com/vercel/next.js/issues/23568#issuecomment-814971318
+ErrorPage.getInitialProps = (ctx: NextPageContext) => {
+  const { res, err } = ctx;
+  const statusCode = res?.statusCode ?? err?.statusCode ?? 500;
+  return { statusCode };
+};

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

@@ -34,12 +34,12 @@ import PluginUtils from '~/server/plugins/plugin-utils';
 import ConfigLoader from '~/server/service/config-loader';
 import ConfigLoader from '~/server/service/config-loader';
 import {
 import {
   useCurrentUser, /* useSearchServiceConfigured, */ useIsAclEnabled, useIsMailerSetup, useIsSearchServiceReachable, useSiteUrl,
   useCurrentUser, /* useSearchServiceConfigured, */ useIsAclEnabled, useIsMailerSetup, useIsSearchServiceReachable, useSiteUrl,
-  useAuditLogEnabled, useAuditLogAvailableActions,
+  useAuditLogEnabled, useAuditLogAvailableActions, useIsSearchPage, useCustomizeTitle,
 } from '~/stores/context';
 } from '~/stores/context';
 import { useIsMaintenanceMode } from '~/stores/maintenanceMode';
 import { useIsMaintenanceMode } from '~/stores/maintenanceMode';
 
 
 import {
 import {
-  CommonProps, getServerSideCommonProps, getNextI18NextConfig,
+  CommonProps, getServerSideCommonProps, getNextI18NextConfig, useCustomTitle,
 } from '../utils/commons';
 } from '../utils/commons';
 
 
 
 
@@ -82,6 +82,7 @@ type Props = CommonProps & {
   auditLogEnabled: boolean,
   auditLogEnabled: boolean,
   auditLogAvailableActions: SupportedActionType[],
   auditLogAvailableActions: SupportedActionType[],
 
 
+  customizeTitle: string,
   siteUrl: string,
   siteUrl: string,
 };
 };
 
 
@@ -191,6 +192,7 @@ const AdminMarkdownSettingsPage: NextPage<Props> = (props: Props) => {
 
 
   const targetPage = getTargetPageToRender(adminPagesMap, pagePathKeys);
   const targetPage = getTargetPageToRender(adminPagesMap, pagePathKeys);
 
 
+  useIsSearchPage(false);
   useCurrentUser(props.currentUser != null ? JSON.parse(props.currentUser) : null);
   useCurrentUser(props.currentUser != null ? JSON.parse(props.currentUser) : null);
   useIsMailerSetup(props.isMailerSetup);
   useIsMailerSetup(props.isMailerSetup);
   useIsMaintenanceMode(props.isMaintenanceMode);
   useIsMaintenanceMode(props.isMaintenanceMode);
@@ -206,6 +208,8 @@ const AdminMarkdownSettingsPage: NextPage<Props> = (props: Props) => {
   useAuditLogEnabled(props.auditLogEnabled);
   useAuditLogEnabled(props.auditLogEnabled);
   useAuditLogAvailableActions(props.auditLogAvailableActions);
   useAuditLogAvailableActions(props.auditLogAvailableActions);
 
 
+  useCustomizeTitle(props.customizeTitle);
+
   const injectableContainers: Container<any>[] = [];
   const injectableContainers: Container<any>[] = [];
 
 
   if (isClient()) {
   if (isClient()) {
@@ -268,7 +272,7 @@ const AdminMarkdownSettingsPage: NextPage<Props> = (props: Props) => {
 
 
   return (
   return (
     <Provider inject={[...injectableContainers, ...adminSecurityContainers]}>
     <Provider inject={[...injectableContainers, ...adminSecurityContainers]}>
-      <AdminLayout title={targetPage.title} selectedNavOpt={firstPath}>
+      <AdminLayout title={useCustomTitle(props, targetPage.title)} selectedNavOpt={firstPath} componentTitle={targetPage.title}>
         {targetPage.component}
         {targetPage.component}
       </AdminLayout>
       </AdminLayout>
     </Provider>
     </Provider>
@@ -298,6 +302,7 @@ async function injectServerConfigurations(context: GetServerSidePropsContext, pr
 
 
   props.auditLogEnabled = crowi.configManager.getConfig('crowi', 'app:auditLogEnabled');
   props.auditLogEnabled = crowi.configManager.getConfig('crowi', 'app:auditLogEnabled');
   props.auditLogAvailableActions = activityService.getAvailableActions(false);
   props.auditLogAvailableActions = activityService.getAvailableActions(false);
+  props.customizeTitle = crowi.configManager.getConfig('crowi', 'customize:title');
 }
 }
 
 
 /**
 /**

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

@@ -18,7 +18,7 @@ import { ISidebarConfig } from '~/interfaces/sidebar-config';
 import { IUserUISettings } from '~/interfaces/user-ui-settings';
 import { IUserUISettings } from '~/interfaces/user-ui-settings';
 import { UserUISettingsModel } from '~/server/models/user-ui-settings';
 import { UserUISettingsModel } from '~/server/models/user-ui-settings';
 import {
 import {
-  useCurrentUser,
+  useCurrentUser, useIsSearchPage,
   useIsSearchServiceConfigured, useIsSearchServiceReachable,
   useIsSearchServiceConfigured, useIsSearchServiceReachable,
   useCsrfToken, useIsSearchScopeChildrenAsDefault,
   useCsrfToken, useIsSearchScopeChildrenAsDefault,
   useRegistrationWhiteList, useShowPageLimitationXL,
   useRegistrationWhiteList, useShowPageLimitationXL,
@@ -85,6 +85,8 @@ const MePage: NextPage<Props> = (props: Props) => {
 
 
   const targetPage = getTargetPageToRender(mePagesMap, pagePathKeys);
   const targetPage = getTargetPageToRender(mePagesMap, pagePathKeys);
 
 
+  useIsSearchPage(false);
+
   useCurrentUser(props.currentUser ?? null);
   useCurrentUser(props.currentUser ?? null);
 
 
   useRegistrationWhiteList(props.registrationWhiteList);
   useRegistrationWhiteList(props.registrationWhiteList);

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

@@ -15,7 +15,7 @@ import { CrowiRequest } from '~/interfaces/crowi-request';
 import { RendererConfig } from '~/interfaces/services/renderer';
 import { RendererConfig } from '~/interfaces/services/renderer';
 import { IShareLinkHasId } from '~/interfaces/share-link';
 import { IShareLinkHasId } from '~/interfaces/share-link';
 import {
 import {
-  useCurrentUser, useCurrentPagePath, useCurrentPathname, useCurrentPageId, useRendererConfig,
+  useCurrentUser, useCurrentPagePath, useCurrentPathname, useCurrentPageId, useRendererConfig, useIsSearchPage,
   useShareLinkId, useIsSearchServiceConfigured, useIsSearchServiceReachable, useIsSearchScopeChildrenAsDefault,
   useShareLinkId, useIsSearchServiceConfigured, useIsSearchServiceReachable, useIsSearchScopeChildrenAsDefault,
 } from '~/stores/context';
 } from '~/stores/context';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
@@ -41,6 +41,7 @@ type Props = CommonProps & {
 };
 };
 
 
 const SharedPage: NextPage<Props> = (props: Props) => {
 const SharedPage: NextPage<Props> = (props: Props) => {
+  useIsSearchPage(false);
   useShareLinkId(props.shareLink?._id);
   useShareLinkId(props.shareLink?._id);
   useCurrentPageId(props.shareLink?.relatedPage._id);
   useCurrentPageId(props.shareLink?.relatedPage._id);
   useCurrentPagePath(props.shareLink?.relatedPage.path);
   useCurrentPagePath(props.shareLink?.relatedPage.path);

+ 2 - 1
packages/app/src/pages/tags.page.tsx

@@ -16,7 +16,7 @@ import { useSWRxTagsList } from '~/stores/tag';
 
 
 import { BasicLayout } from '../components/Layout/BasicLayout';
 import { BasicLayout } from '../components/Layout/BasicLayout';
 import {
 import {
-  useCurrentUser,
+  useCurrentUser, useIsSearchPage,
   useIsSearchServiceConfigured, useIsSearchServiceReachable,
   useIsSearchServiceConfigured, useIsSearchServiceReachable,
   useIsSearchScopeChildrenAsDefault,
   useIsSearchScopeChildrenAsDefault,
 } from '../stores/context';
 } from '../stores/context';
@@ -55,6 +55,7 @@ const TagPage: NextPage<CommonProps> = (props: Props) => {
   const isLoading = tagDataList === undefined && error == null;
   const isLoading = tagDataList === undefined && error == null;
   const classNames: string[] = [];
   const classNames: string[] = [];
 
 
+  useIsSearchPage(false);
   useIsSearchServiceConfigured(props.isSearchServiceConfigured);
   useIsSearchServiceConfigured(props.isSearchServiceConfigured);
   useIsSearchServiceReachable(props.isSearchServiceReachable);
   useIsSearchServiceReachable(props.isSearchServiceReachable);
   useIsSearchScopeChildrenAsDefault(props.isSearchScopeChildrenAsDefault);
   useIsSearchScopeChildrenAsDefault(props.isSearchScopeChildrenAsDefault);

+ 2 - 1
packages/app/src/pages/trash.page.tsx

@@ -15,7 +15,7 @@ import GrowiContextualSubNavigation from '../components/Navbar/GrowiContextualSu
 import {
 import {
   useCurrentUser, useCurrentPageId, useCurrentPagePath, useCurrentPathname,
   useCurrentUser, useCurrentPageId, useCurrentPagePath, useCurrentPathname,
   useIsSearchServiceConfigured, useIsSearchServiceReachable,
   useIsSearchServiceConfigured, useIsSearchServiceReachable,
-  useIsSearchScopeChildrenAsDefault,
+  useIsSearchScopeChildrenAsDefault, useIsSearchPage,
 } from '../stores/context';
 } from '../stores/context';
 
 
 import {
 import {
@@ -41,6 +41,7 @@ const TrashPage: NextPage<CommonProps> = (props: Props) => {
   useIsSearchServiceReachable(props.isSearchServiceReachable);
   useIsSearchServiceReachable(props.isSearchServiceReachable);
   useIsSearchScopeChildrenAsDefault(props.isSearchScopeChildrenAsDefault);
   useIsSearchScopeChildrenAsDefault(props.isSearchScopeChildrenAsDefault);
 
 
+  useIsSearchPage(false);
   useCurrentPageId(null);
   useCurrentPageId(null);
   useCurrentPathname('/trash');
   useCurrentPathname('/trash');
   useCurrentPagePath('/trash');
   useCurrentPagePath('/trash');

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

@@ -258,6 +258,10 @@ export const useShowPageLimitationXL = (initialData?: number): SWRResponse<numbe
   return useStaticSWR('showPageLimitationXL', initialData);
   return useStaticSWR('showPageLimitationXL', initialData);
 };
 };
 
 
+export const useCustomizeTitle = (initialData?: string): SWRResponse<string, Error> => {
+  return useStaticSWR('CustomizeTitle', initialData);
+};
+
 /** **********************************************************
 /** **********************************************************
  *                     Computed contexts
  *                     Computed contexts
  *********************************************************** */
  *********************************************************** */

+ 12 - 0
packages/app/src/styles/atoms/_code.scss

@@ -0,0 +1,12 @@
+@use '~/styles/variables' as var;
+@use '~/styles/bootstrap/init' as bs;
+@use '~/styles/atoms/mixins' as atm;
+
+/*
+ * style of inline-code
+ */
+:not(pre) {
+  > code {
+    @include atm.code-inline;
+  }
+}

+ 15 - 0
packages/app/src/styles/atoms/_mixins.scss

@@ -0,0 +1,15 @@
+@use '~/styles/variables' as var;
+@use '~/styles/bootstrap/init' as bs;
+
+@mixin code-inline {
+  padding: 2px 4px;
+  font-family: var.$font-family-monospace-not-strictly;
+  border: 1px solid;
+  border-radius: bs.$border-radius;
+}
+
+@mixin code-inline-color($color-inline-code,$bgcolor-inline-code, $bordercolor-inline-code) {
+  color: $color-inline-code;
+  background-color: $bgcolor-inline-code;
+  border-color: $bordercolor-inline-code;
+}

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

@@ -32,6 +32,7 @@
 @import 'atoms/buttons';
 @import 'atoms/buttons';
 @import 'atoms/spinners';
 @import 'atoms/spinners';
 @import 'atoms/custom_control';
 @import 'atoms/custom_control';
+@import 'atoms/code';
 
 
 // molecules
 // molecules
 @import 'molecules/toastr';
 @import 'molecules/toastr';

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

@@ -2,7 +2,7 @@
 @use '../bootstrap/init' as *;
 @use '../bootstrap/init' as *;
 @use '../mixins';
 @use '../mixins';
 @use './mixins/tables'; // comment out and use _reboot-bootstrap-tables instead -- 2020.05.28 Yuki Takei
 @use './mixins/tables'; // comment out and use _reboot-bootstrap-tables instead -- 2020.05.28 Yuki Takei
-
+@use '../atoms/mixins' as atm;
 //
 //
 //== Apply to Bootstrap
 //== Apply to Bootstrap
 //
 //
@@ -49,10 +49,14 @@ $theme-colors: map-merge($theme-colors, ( primary: $primary ));
 // determine variables with bootstrap function (These variables can be used after importing bootstrap above)
 // determine variables with bootstrap function (These variables can be used after importing bootstrap above)
 $color-modal-header: color-yiq($primary) !default;
 $color-modal-header: color-yiq($primary) !default;
 
 
+:not(pre) {
+  > code {
+    @include atm.code-inline-color($color-inline-code, $bgcolor-inline-code, $bordercolor-inline-code);
+  }
+}
+
 .code-inline {
 .code-inline {
-  color: $color-inline-code;
-  background-color: $bgcolor-inline-code;
-  border-color: $bordercolor-inline-code;
+  @include atm.code-inline-color($color-inline-code, $bgcolor-inline-code, $bordercolor-inline-code);
 }
 }
 
 
 .code-highlighted {
 .code-highlighted {