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

Merge branch 'master' into imprv/112501-do-not-use-api-for-fetching-pages-when-using-shared-pages

Shun Miyazawa 3 лет назад
Родитель
Сommit
fdb69fd1c8
27 измененных файлов с 168 добавлено и 94 удалено
  1. 10 2
      .github/workflows/release-rc.yml
  2. 1 1
      .github/workflows/release-slackbot-proxy.yml
  3. 10 1
      .github/workflows/release.yml
  4. 6 2
      packages/app/docker/Dockerfile
  5. 7 0
      packages/app/public/static/locales/en_US/translation.json
  6. 8 0
      packages/app/public/static/locales/ja_JP/translation.json
  7. 7 0
      packages/app/public/static/locales/zh_CN/translation.json
  8. 31 42
      packages/app/src/components/Admin/Customize/CustomizeLogoSetting.tsx
  9. 8 8
      packages/app/src/components/Navbar/GrowiNavbar.tsx
  10. 5 3
      packages/app/src/components/Page/TagEditModal.jsx
  11. 3 1
      packages/app/src/components/Page/TagsInput.tsx
  12. 6 1
      packages/app/src/components/PageEditor/ScrollSyncHelper.js
  13. 1 1
      packages/app/src/pages/[[...path]].page.tsx
  14. 2 2
      packages/app/src/pages/_app.page.tsx
  15. 4 1
      packages/app/src/pages/admin/customize.page.tsx
  16. 5 4
      packages/app/src/pages/utils/commons.ts
  17. 8 0
      packages/app/src/server/middlewares/certify-brand-logo.ts
  18. 6 0
      packages/app/src/server/middlewares/login-required.js
  19. 0 3
      packages/app/src/server/models/config.ts
  20. 0 14
      packages/app/src/server/routes/apiv3/customize-setting.js
  21. 10 0
      packages/app/src/server/routes/attachment.js
  22. 4 0
      packages/app/src/server/routes/index.js
  23. 11 0
      packages/app/src/server/service/attachment.js
  24. 1 1
      packages/app/src/services/renderer/rehype-plugins/add-line-number-attribute.ts
  25. 5 2
      packages/app/src/services/renderer/remark-plugins/plantuml.ts
  26. 3 3
      packages/app/src/services/renderer/renderer.tsx
  27. 6 2
      packages/app/src/stores/context.tsx

+ 10 - 2
.github/workflows/release-rc.yml

@@ -11,6 +11,10 @@ jobs:
 
 
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
 
 
+    strategy:
+      matrix:
+        platform: [linux/amd64, linux/arm64]
+
     steps:
     steps:
     - uses: actions/checkout@v3
     - uses: actions/checkout@v3
       with:
       with:
@@ -22,7 +26,7 @@ jobs:
 
 
     - name: Docker meta
     - name: Docker meta
       id: meta
       id: meta
-      uses: docker/metadata-action@v3
+      uses: docker/metadata-action@v4
       with:
       with:
         images: weseek/growi,ghcr.io/weseek/growi
         images: weseek/growi,ghcr.io/weseek/growi
         tags: |
         tags: |
@@ -40,6 +44,10 @@ jobs:
         username: wsmoogle
         username: wsmoogle
         password: ${{ secrets.DOCKER_REGISTRY_ON_GITHUB_PASSWORD }}
         password: ${{ secrets.DOCKER_REGISTRY_ON_GITHUB_PASSWORD }}
 
 
+    - name: Set up QEMU
+      if: ${{ matrix.platform == 'linux/arm64' }}
+      uses: docker/setup-qemu-action@v1
+
     - name: Set up Docker Buildx
     - name: Set up Docker Buildx
       uses: docker/setup-buildx-action@v2
       uses: docker/setup-buildx-action@v2
 
 
@@ -48,7 +56,7 @@ jobs:
       with:
       with:
         context: .
         context: .
         file: ./packages/app/docker/Dockerfile
         file: ./packages/app/docker/Dockerfile
-        platforms: linux/amd64
+        platforms: ${{ matrix.platform }}
         push: true
         push: true
         builder: ${{ steps.buildx.outputs.name }}
         builder: ${{ steps.buildx.outputs.name }}
         cache-from: type=gha
         cache-from: type=gha

+ 1 - 1
.github/workflows/release-slackbot-proxy.yml

@@ -24,7 +24,7 @@ jobs:
 
 
     - name: Docker meta
     - name: Docker meta
       id: meta
       id: meta
-      uses: docker/metadata-action@v3
+      uses: docker/metadata-action@v4
       with:
       with:
         images: weseek/growi-slackbot-proxy,ghcr.io/weseek/growi-slackbot-proxy,asia.gcr.io/${{ secrets.GCP_PRJ_ID_SLACKBOT_PROXY }}/growi-slackbot-proxy
         images: weseek/growi-slackbot-proxy,ghcr.io/weseek/growi-slackbot-proxy,asia.gcr.io/${{ secrets.GCP_PRJ_ID_SLACKBOT_PROXY }}/growi-slackbot-proxy
         tags: |
         tags: |

+ 10 - 1
.github/workflows/release.yml

@@ -126,6 +126,10 @@ jobs:
 
 
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
 
 
+    strategy:
+      matrix:
+        platform: [linux/amd64, linux/arm64]
+
     steps:
     steps:
     - uses: actions/checkout@v3
     - uses: actions/checkout@v3
       with:
       with:
@@ -156,6 +160,10 @@ jobs:
         username: wsmoogle
         username: wsmoogle
         password: ${{ secrets.DOCKER_REGISTRY_ON_GITHUB_PASSWORD }}
         password: ${{ secrets.DOCKER_REGISTRY_ON_GITHUB_PASSWORD }}
 
 
+    - name: Set up QEMU
+      if: ${{ matrix.platform == 'linux/arm64' }}
+      uses: docker/setup-qemu-action@v1
+
     - name: Set up Docker Buildx
     - name: Set up Docker Buildx
       uses: docker/setup-buildx-action@v2
       uses: docker/setup-buildx-action@v2
 
 
@@ -164,7 +172,7 @@ jobs:
       with:
       with:
         context: .
         context: .
         file: ./packages/app/docker/Dockerfile
         file: ./packages/app/docker/Dockerfile
-        platforms: linux/amd64
+        platforms: ${{ matrix.platform }}
         push: true
         push: true
         builder: ${{ steps.buildx.outputs.name }}
         builder: ${{ steps.buildx.outputs.name }}
         cache-from: type=gha
         cache-from: type=gha
@@ -185,6 +193,7 @@ jobs:
         channel: '#release'
         channel: '#release'
         url: ${{ secrets.SLACK_WEBHOOK_URL }}
         url: ${{ secrets.SLACK_WEBHOOK_URL }}
         created_tag: 'v${{ needs.create-github-release.outputs.RELEASED_VERSION }}'
         created_tag: 'v${{ needs.create-github-release.outputs.RELEASED_VERSION }}'
+        message: '*Release v${{ needs.create-github-release.outputs.RELEASED_VERSION }} (${{ matrix.platform }})* Succeeded'
 
 
     - name: Check whether workspace is clean
     - name: Check whether workspace is clean
       run: |
       run: |

+ 6 - 2
packages/app/docker/Dockerfile

@@ -28,11 +28,15 @@ ENV nodeModulesGrowiPackagesDir ${optDir}/node_modules/@growi
 # expect a string seperated by commas (e.g. "A,B")
 # expect a string seperated by commas (e.g. "A,B")
 ENV removeNodeModulesSymlinkPaths ${nodeModulesGrowiPackagesDir}/slackbot-proxy
 ENV removeNodeModulesSymlinkPaths ${nodeModulesGrowiPackagesDir}/slackbot-proxy
 
 
+RUN set -eux; \
+	apt-get update; \
+	apt-get install -y python3 build-essential;
+
 # copy files
 # copy files
 COPY --from=packages-json-picker ${optDir} .
 COPY --from=packages-json-picker ${optDir} .
 
 
-# setup
-RUN yarn config set network-timeout 300000
+# setup (with network-timeout = 1 hour)
+RUN yarn config set network-timeout 3600000
 RUN npx -y lerna bootstrap -- --frozen-lockfile
 RUN npx -y lerna bootstrap -- --frozen-lockfile
 
 
 # remove unnecessary symlinks
 # remove unnecessary symlinks

+ 7 - 0
packages/app/public/static/locales/en_US/translation.json

@@ -798,5 +798,12 @@
   "v5_page_migration": {
   "v5_page_migration": {
     "page_tree_not_avaliable" : "Page tree feature is not available yet.",
     "page_tree_not_avaliable" : "Page tree feature is not available yet.",
     "go_to_settings": "Go to settings to enable the feature"
     "go_to_settings": "Go to settings to enable the feature"
+  },
+  "tag_edit_modal": {
+    "edit_tags": "Edit Tags",
+    "done": "Done",
+    "tags_input": {
+      "tag_name": "tag name"
+    }
   }
   }
 }
 }

+ 8 - 0
packages/app/public/static/locales/ja_JP/translation.json

@@ -80,6 +80,7 @@
   "View diff": "差分を表示",
   "View diff": "差分を表示",
   "No diff": "差分なし",
   "No diff": "差分なし",
   "User ID": "ユーザーID",
   "User ID": "ユーザーID",
+  "User Settings": "ユーザー設定",
   "User Information": "ユーザー情報",
   "User Information": "ユーザー情報",
   "Basic Info": "ユーザーの基本情報",
   "Basic Info": "ユーザーの基本情報",
   "Name": "名前",
   "Name": "名前",
@@ -797,5 +798,12 @@
   "v5_page_migration": {
   "v5_page_migration": {
     "page_tree_not_avaliable" : "Page Tree 機能は現在使用できません。",
     "page_tree_not_avaliable" : "Page Tree 機能は現在使用できません。",
     "go_to_settings": "設定する"
     "go_to_settings": "設定する"
+  },
+  "tag_edit_modal": {
+    "edit_tags": "タグの編集",
+    "done": "完了",
+    "tags_input": {
+      "tag_name": "タグ名"
+    }
   }
   }
 }
 }

+ 7 - 0
packages/app/public/static/locales/zh_CN/translation.json

@@ -802,5 +802,12 @@
   "v5_page_migration": {
   "v5_page_migration": {
     "page_tree_not_avaliable": "Page Tree 功能不可用",
     "page_tree_not_avaliable": "Page Tree 功能不可用",
     "go_to_settings": "进入设置,启用该功能"
     "go_to_settings": "进入设置,启用该功能"
+  },
+  "tag_edit_modal": {
+    "edit_tags": "编辑标签",
+    "done": "完毕",
+    "tags_input": {
+      "tag_name": "标签名称"
+    }
   }
   }
 }
 }

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

@@ -1,45 +1,34 @@
-import React, { useCallback, useEffect, useState } from 'react';
+import React, { useCallback, useMemo, useState } from 'react';
 
 
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 
 
 import { toastError, toastSuccess } from '~/client/util/apiNotification';
 import { toastError, toastSuccess } from '~/client/util/apiNotification';
 import {
 import {
-  apiv3Delete, apiv3Get, apiv3PostForm, apiv3Put,
+  apiv3Delete, apiv3PostForm, apiv3Put,
 } from '~/client/util/apiv3-client';
 } from '~/client/util/apiv3-client';
 import ImageCropModal from '~/components/Common/ImageCropModal';
 import ImageCropModal from '~/components/Common/ImageCropModal';
+import { useIsDefaultLogo, useIsCustomizedLogoUploaded } from '~/stores/context';
 
 
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 
 
+
 const DEFAULT_LOGO = '/images/logo.svg';
 const DEFAULT_LOGO = '/images/logo.svg';
+const CUSTOMIZED_LOGO = '/attachment/brand-logo';
 
 
 const CustomizeLogoSetting = (): JSX.Element => {
 const CustomizeLogoSetting = (): JSX.Element => {
 
 
   const { t } = useTranslation();
   const { t } = useTranslation();
+  const { data: isDefaultLogo } = useIsDefaultLogo();
+  const { data: isCustomizedLogoUploaded, mutate: mutateIsCustomizedLogoUploaded } = useIsCustomizedLogoUploaded();
 
 
   const [uploadLogoSrc, setUploadLogoSrc] = useState<ArrayBuffer | string | null>(null);
   const [uploadLogoSrc, setUploadLogoSrc] = useState<ArrayBuffer | string | null>(null);
   const [isImageCropModalShow, setIsImageCropModalShow] = useState<boolean>(false);
   const [isImageCropModalShow, setIsImageCropModalShow] = useState<boolean>(false);
-  const [isDefaultLogo, setIsDefaultLogo] = useState<boolean>(true);
+  const [isDefaultLogoSelected, setIsDefaultLogoSelected] = useState<boolean>(isDefaultLogo ?? true);
   const [retrieveError, setRetrieveError] = useState<any>();
   const [retrieveError, setRetrieveError] = useState<any>();
-  const [customizedLogoSrc, setCustomizedLogoSrc] = useState< string | null >(null);
-
-  const retrieveData = useCallback(async() => {
-    try {
-      const response = await apiv3Get('/customize-setting/customize-logo');
-      const { isDefaultLogo: _isDefaultLogo, customizedLogoSrc } = response.data;
-      const isDefaultLogo = _isDefaultLogo ?? true;
-
-      setIsDefaultLogo(isDefaultLogo);
-      setCustomizedLogoSrc(customizedLogoSrc);
-    }
-    catch (err) {
-      setRetrieveError(err);
-      throw new Error('Failed to fetch data');
-    }
-  }, []);
 
 
-  useEffect(() => {
-    retrieveData();
-  }, [retrieveData]);
+  const currentLogo = useMemo(() => {
+    return isDefaultLogo ? DEFAULT_LOGO : CUSTOMIZED_LOGO;
+  }, [isDefaultLogo]);
 
 
   const onSelectFile = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
   const onSelectFile = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
     if (e.target.files != null && e.target.files.length > 0) {
     if (e.target.files != null && e.target.files.length > 0) {
@@ -52,22 +41,18 @@ const CustomizeLogoSetting = (): JSX.Element => {
 
 
   const onClickSubmit = useCallback(async() => {
   const onClickSubmit = useCallback(async() => {
     try {
     try {
-      const response = await apiv3Put('/customize-setting/customize-logo', {
-        isDefaultLogo,
-      });
-      const { customizedParams } = response.data;
-      setIsDefaultLogo(customizedParams.isDefaultLogo);
+      await apiv3Put('/customize-setting/customize-logo', { isDefaultLogo: isDefaultLogoSelected });
       toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.custom_logo'), ns: 'commons' }));
       toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.custom_logo'), ns: 'commons' }));
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);
     }
     }
-  }, [t, isDefaultLogo]);
+  }, [t, isDefaultLogoSelected]);
 
 
   const onClickDeleteBtn = useCallback(async() => {
   const onClickDeleteBtn = useCallback(async() => {
     try {
     try {
       await apiv3Delete('/customize-setting/delete-brand-logo');
       await apiv3Delete('/customize-setting/delete-brand-logo');
-      setCustomizedLogoSrc(null);
+      mutateIsCustomizedLogoUploaded(false);
       toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.current_logo'), ns: 'commons' }));
       toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.current_logo'), ns: 'commons' }));
     }
     }
     catch (err) {
     catch (err) {
@@ -75,15 +60,15 @@ const CustomizeLogoSetting = (): JSX.Element => {
       setRetrieveError(err);
       setRetrieveError(err);
       throw new Error('Failed to delete logo');
       throw new Error('Failed to delete logo');
     }
     }
-  }, [t]);
+  }, [mutateIsCustomizedLogoUploaded, t]);
 
 
 
 
   const processImageCompletedHandler = useCallback(async(croppedImage) => {
   const processImageCompletedHandler = useCallback(async(croppedImage) => {
     try {
     try {
       const formData = new FormData();
       const formData = new FormData();
       formData.append('file', croppedImage);
       formData.append('file', croppedImage);
-      const { data } = await apiv3PostForm('/customize-setting/upload-brand-logo', formData);
-      setCustomizedLogoSrc(data.attachment.filePathProxied);
+      await apiv3PostForm('/customize-setting/upload-brand-logo', formData);
+      mutateIsCustomizedLogoUploaded(true);
       toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.current_logo'), ns: 'commons' }));
       toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.current_logo'), ns: 'commons' }));
     }
     }
     catch (err) {
     catch (err) {
@@ -91,7 +76,7 @@ const CustomizeLogoSetting = (): JSX.Element => {
       setRetrieveError(err);
       setRetrieveError(err);
       throw new Error('Failed to upload brand logo');
       throw new Error('Failed to upload brand logo');
     }
     }
-  }, [t]);
+  }, [mutateIsCustomizedLogoUploaded, t]);
 
 
   return (
   return (
     <React.Fragment>
     <React.Fragment>
@@ -109,8 +94,8 @@ const CustomizeLogoSetting = (): JSX.Element => {
                       className="custom-control-input"
                       className="custom-control-input"
                       form="formImageType"
                       form="formImageType"
                       name="imagetypeForm[isDefaultLogo]"
                       name="imagetypeForm[isDefaultLogo]"
-                      checked={isDefaultLogo}
-                      onChange={() => { setIsDefaultLogo(true) }}
+                      checked={isDefaultLogoSelected}
+                      onChange={() => { setIsDefaultLogoSelected(true) }}
                     />
                     />
                     <label className="custom-control-label" htmlFor="radioDefaultLogo">
                     <label className="custom-control-label" htmlFor="radioDefaultLogo">
                       {t('admin:customize_settings.default_logo')}
                       {t('admin:customize_settings.default_logo')}
@@ -128,8 +113,8 @@ const CustomizeLogoSetting = (): JSX.Element => {
                       className="custom-control-input"
                       className="custom-control-input"
                       form="formImageType"
                       form="formImageType"
                       name="imagetypeForm[isDefaultLogo]"
                       name="imagetypeForm[isDefaultLogo]"
-                      checked={!isDefaultLogo}
-                      onChange={() => { setIsDefaultLogo(false) }}
+                      checked={!isDefaultLogoSelected}
+                      onChange={() => { setIsDefaultLogoSelected(false) }}
                     />
                     />
                     <label className="custom-control-label" htmlFor="radioUploadLogo">
                     <label className="custom-control-label" htmlFor="radioUploadLogo">
                       { t('admin:customize_settings.upload_logo') }
                       { t('admin:customize_settings.upload_logo') }
@@ -141,11 +126,15 @@ const CustomizeLogoSetting = (): JSX.Element => {
                     { t('admin:customize_settings.current_logo') }
                     { t('admin:customize_settings.current_logo') }
                   </label>
                   </label>
                   <div className="col-sm-8 col-12">
                   <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_settings.delete_logo') }
-                      </button>
+                    {isCustomizedLogoUploaded && (
+                      <>
+                        <p>
+                          <img src='/attachment/brand-logo' className="picture picture-lg " id="settingBrandLogo" width="64" />
+                        </p>
+                        <button type="button" className="btn btn-danger" onClick={onClickDeleteBtn}>
+                          { t('admin:customize_settings.delete_logo') }
+                        </button>
+                      </>
                     )}
                     )}
                   </div>
                   </div>
                 </div>
                 </div>

+ 8 - 8
packages/app/src/components/Navbar/GrowiNavbar.tsx

@@ -11,7 +11,7 @@ import { useRipple } from 'react-use-ripple';
 import { UncontrolledTooltip } from 'reactstrap';
 import { UncontrolledTooltip } from 'reactstrap';
 
 
 import {
 import {
-  useIsSearchPage, useIsGuestUser, useIsSearchServiceConfigured, useAppTitle, useConfidential, useCustomizedLogoSrc,
+  useIsSearchPage, useIsGuestUser, useIsSearchServiceConfigured, useAppTitle, useConfidential, useIsDefaultLogo,
 } from '~/stores/context';
 } from '~/stores/context';
 import { usePageCreateModal } from '~/stores/modal';
 import { usePageCreateModal } from '~/stores/modal';
 import { useCurrentPagePath } from '~/stores/page';
 import { useCurrentPagePath } from '~/stores/page';
@@ -122,16 +122,16 @@ const Confidential: FC<ConfidentialProps> = memo((props: ConfidentialProps): JSX
 Confidential.displayName = 'Confidential';
 Confidential.displayName = 'Confidential';
 
 
 interface NavbarLogoProps {
 interface NavbarLogoProps {
-  logoSrc?: string,
+  isDefaultLogo?: boolean
 }
 }
 
 
 const GrowiNavbarLogo: FC<NavbarLogoProps> = memo((props: NavbarLogoProps) => {
 const GrowiNavbarLogo: FC<NavbarLogoProps> = memo((props: NavbarLogoProps) => {
-  const { logoSrc } = props;
+  const { isDefaultLogo } = props;
 
 
-  return logoSrc != null
+  return isDefaultLogo
+    ? <GrowiLogo />
     // eslint-disable-next-line @next/next/no-img-element
     // eslint-disable-next-line @next/next/no-img-element
-    ? (<img src={logoSrc} alt="custom logo" className="picture picture-lg p-2 mx-2" id="settingBrandLogo" width="32" />)
-    : <GrowiLogo />;
+    : (<img src='/attachment/brand-logo' alt="custom logo" className="picture picture-lg p-2 mx-2" id="settingBrandLogo" width="32" />);
 });
 });
 
 
 GrowiNavbarLogo.displayName = 'GrowiNavbarLogo';
 GrowiNavbarLogo.displayName = 'GrowiNavbarLogo';
@@ -151,7 +151,7 @@ export const GrowiNavbar = (props: Props): JSX.Element => {
   const { data: isSearchServiceConfigured } = useIsSearchServiceConfigured();
   const { data: isSearchServiceConfigured } = useIsSearchServiceConfigured();
   const { data: isDeviceSmallerThanMd } = useIsDeviceSmallerThanMd();
   const { data: isDeviceSmallerThanMd } = useIsDeviceSmallerThanMd();
   const { data: isSearchPage } = useIsSearchPage();
   const { data: isSearchPage } = useIsSearchPage();
-  const { data: customizedLogoSrc } = useCustomizedLogoSrc();
+  const { data: isDefaultLogo } = useIsDefaultLogo();
 
 
   return (
   return (
     <nav id="grw-navbar" className={`navbar grw-navbar ${styles['grw-navbar']} navbar-expand navbar-dark sticky-top mb-0 px-0`}>
     <nav id="grw-navbar" className={`navbar grw-navbar ${styles['grw-navbar']} navbar-expand navbar-dark sticky-top mb-0 px-0`}>
@@ -159,7 +159,7 @@ export const GrowiNavbar = (props: Props): JSX.Element => {
       <div className="navbar-brand mr-0">
       <div className="navbar-brand mr-0">
         <Link href="/" prefetch={false}>
         <Link href="/" prefetch={false}>
           <a className="grw-logo d-block">
           <a className="grw-logo d-block">
-            <GrowiNavbarLogo logoSrc={customizedLogoSrc}/>
+            <GrowiNavbarLogo isDefaultLogo={isDefaultLogo} />
           </a>
           </a>
         </Link>
         </Link>
       </div>
       </div>

+ 5 - 3
packages/app/src/components/Page/TagEditModal.jsx

@@ -1,6 +1,7 @@
 import React, { useState, useEffect } from 'react';
 import React, { useState, useEffect } from 'react';
-import PropTypes from 'prop-types';
 
 
+import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 import {
 import {
   Modal, ModalHeader, ModalBody, ModalFooter,
   Modal, ModalHeader, ModalBody, ModalFooter,
 } from 'reactstrap';
 } from 'reactstrap';
@@ -9,6 +10,7 @@ import TagsInput from './TagsInput';
 
 
 function TagEditModal(props) {
 function TagEditModal(props) {
   const [tags, setTags] = useState([]);
   const [tags, setTags] = useState([]);
+  const { t } = useTranslation();
 
 
   function onTagsUpdatedByTagsInput(tags) {
   function onTagsUpdatedByTagsInput(tags) {
     setTags(tags);
     setTags(tags);
@@ -37,14 +39,14 @@ function TagEditModal(props) {
   return (
   return (
     <Modal isOpen={props.isOpen} toggle={closeModalHandler} id="edit-tag-modal" autoFocus={false}>
     <Modal isOpen={props.isOpen} toggle={closeModalHandler} id="edit-tag-modal" autoFocus={false}>
       <ModalHeader tag="h4" toggle={closeModalHandler} className="bg-primary text-light">
       <ModalHeader tag="h4" toggle={closeModalHandler} className="bg-primary text-light">
-        Edit Tags
+        {t('tag_edit_modal.edit_tags')}
       </ModalHeader>
       </ModalHeader>
       <ModalBody>
       <ModalBody>
         <TagsInput tags={tags} onTagsUpdated={onTagsUpdatedByTagsInput} autoFocus />
         <TagsInput tags={tags} onTagsUpdated={onTagsUpdatedByTagsInput} autoFocus />
       </ModalBody>
       </ModalBody>
       <ModalFooter>
       <ModalFooter>
         <button type="button" className="btn btn-primary" onClick={handleSubmit}>
         <button type="button" className="btn btn-primary" onClick={handleSubmit}>
-          Done
+          {t('tag_edit_modal.done')}
         </button>
         </button>
       </ModalFooter>
       </ModalFooter>
     </Modal>
     </Modal>

+ 3 - 1
packages/app/src/components/Page/TagsInput.tsx

@@ -2,6 +2,7 @@ import React, {
   FC, useRef, useState, useCallback,
   FC, useRef, useState, useCallback,
 } from 'react';
 } from 'react';
 
 
+import { useTranslation } from 'next-i18next';
 import { AsyncTypeahead } from 'react-bootstrap-typeahead';
 import { AsyncTypeahead } from 'react-bootstrap-typeahead';
 
 
 import { useSWRxTagsSearch } from '~/stores/tag';
 import { useSWRxTagsSearch } from '~/stores/tag';
@@ -20,6 +21,7 @@ type Props = {
 }
 }
 
 
 const TagsInput: FC<Props> = (props: Props) => {
 const TagsInput: FC<Props> = (props: Props) => {
+  const { t } = useTranslation();
   const tagsInputRef = useRef<TypeaheadInstance>(null);
   const tagsInputRef = useRef<TypeaheadInstance>(null);
 
 
   const [resultTags, setResultTags] = useState<string[]>([]);
   const [resultTags, setResultTags] = useState<string[]>([]);
@@ -71,7 +73,7 @@ const TagsInput: FC<Props> = (props: Props) => {
         onSearch={searchHandler}
         onSearch={searchHandler}
         onKeyDown={keyDownHandler}
         onKeyDown={keyDownHandler}
         options={resultTags} // Search result (Some tag names)
         options={resultTags} // Search result (Some tag names)
-        placeholder="tag name"
+        placeholder={t('tag_edit_modal.tags_input.tag_name')}
         autoFocus={props.autoFocus}
         autoFocus={props.autoFocus}
       />
       />
     </div>
     </div>

+ 6 - 1
packages/app/src/components/PageEditor/ScrollSyncHelper.js

@@ -77,6 +77,11 @@ class ScrollSyncHelper {
     }
     }
 
 
     const hiElement = lines[hi];
     const hiElement = lines[hi];
+
+    if (hiElement == null) {
+      return {};
+    }
+
     if (hi >= 1 && hiElement.element.getBoundingClientRect().top > position) {
     if (hi >= 1 && hiElement.element.getBoundingClientRect().top > position) {
       const loElement = lines[lo];
       const loElement = lines[lo];
       const bounds = loElement.element.getBoundingClientRect();
       const bounds = loElement.element.getBoundingClientRect();
@@ -95,7 +100,7 @@ class ScrollSyncHelper {
 
 
   getEditorLineNumberForPageOffset(parentElement, offset) {
   getEditorLineNumberForPageOffset(parentElement, offset) {
     const { previous, next } = this.getLineElementsAtPageOffset(parentElement, offset);
     const { previous, next } = this.getLineElementsAtPageOffset(parentElement, offset);
-    if (previous) {
+    if (previous != null) {
       if (next) {
       if (next) {
         const betweenProgress = (
         const betweenProgress = (
           offset - parentElement.scrollTop - previous.element.getBoundingClientRect().top)
           offset - parentElement.scrollTop - previous.element.getBoundingClientRect().top)

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

@@ -71,7 +71,7 @@ import {
   useIsAclEnabled, useIsSearchPage, useTemplateTagData, useTemplateBodyData, useIsEnabledAttachTitleHeader,
   useIsAclEnabled, useIsSearchPage, useTemplateTagData, useTemplateBodyData, useIsEnabledAttachTitleHeader,
   useCsrfToken, useIsSearchScopeChildrenAsDefault, useCurrentPageId, useCurrentPathname,
   useCsrfToken, useIsSearchScopeChildrenAsDefault, useCurrentPageId, useCurrentPathname,
   useIsSlackConfigured, useRendererConfig,
   useIsSlackConfigured, useRendererConfig,
-  useEditorConfig, useIsAllReplyShown, useIsUploadableFile, useIsUploadableImage, useCustomizedLogoSrc, useIsContainerFluid, useIsNotCreatable,
+  useEditorConfig, useIsAllReplyShown, useIsUploadableFile, useIsUploadableImage, useIsContainerFluid, useIsNotCreatable,
 } from '../stores/context';
 } from '../stores/context';
 
 
 import { NextPageWithLayout } from './_app.page';
 import { NextPageWithLayout } from './_app.page';

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

@@ -11,7 +11,7 @@ import * as nextI18nConfig from '^/config/next-i18next.config';
 import { ActivatePluginService } from '~/client/services/activate-plugin';
 import { ActivatePluginService } from '~/client/services/activate-plugin';
 import { useI18nextHMR } from '~/services/i18next-hmr';
 import { useI18nextHMR } from '~/services/i18next-hmr';
 import {
 import {
-  useAppTitle, useConfidential, useGrowiVersion, useSiteUrl, useCustomizedLogoSrc,
+  useAppTitle, useConfidential, useGrowiVersion, useSiteUrl, useIsDefaultLogo,
 } from '~/stores/context';
 } from '~/stores/context';
 import { SWRConfigValue, swrGlobalConfiguration } from '~/utils/swr-utils';
 import { SWRConfigValue, swrGlobalConfiguration } from '~/utils/swr-utils';
 
 
@@ -63,7 +63,7 @@ function GrowiApp({ Component, pageProps }: GrowiAppProps): JSX.Element {
   useSiteUrl(commonPageProps.siteUrl);
   useSiteUrl(commonPageProps.siteUrl);
   useConfidential(commonPageProps.confidential);
   useConfidential(commonPageProps.confidential);
   useGrowiVersion(commonPageProps.growiVersion);
   useGrowiVersion(commonPageProps.growiVersion);
-  useCustomizedLogoSrc(commonPageProps.customizedLogoSrc);
+  useIsDefaultLogo(commonPageProps.isDefaultLogo);
 
 
   // Use the layout defined at the page level, if available
   // Use the layout defined at the page level, if available
   const getLayout = Component.getLayout ?? (page => page);
   const getLayout = Component.getLayout ?? (page => page);

+ 4 - 1
packages/app/src/pages/admin/customize.page.tsx

@@ -10,7 +10,7 @@ import { Container, Provider } from 'unstated';
 import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
 import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
 import { CrowiRequest } from '~/interfaces/crowi-request';
 import { CrowiRequest } from '~/interfaces/crowi-request';
 import { CommonProps, generateCustomTitle } from '~/pages/utils/commons';
 import { CommonProps, generateCustomTitle } from '~/pages/utils/commons';
-import { useCustomizeTitle, useCurrentUser } from '~/stores/context';
+import { useCustomizeTitle, useCurrentUser, useIsCustomizedLogoUploaded } from '~/stores/context';
 
 
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
 
 
@@ -20,6 +20,7 @@ const CustomizeSettingContents = dynamic(() => import('~/components/Admin/Custom
 
 
 type Props = CommonProps & {
 type Props = CommonProps & {
   customizeTitle: string,
   customizeTitle: string,
+  isCustomizedLogoUploaded: boolean,
 };
 };
 
 
 
 
@@ -27,6 +28,7 @@ const AdminCustomizeSettingsPage: NextPage<Props> = (props) => {
   const { t } = useTranslation('admin');
   const { t } = useTranslation('admin');
   useCustomizeTitle(props.customizeTitle);
   useCustomizeTitle(props.customizeTitle);
   useCurrentUser(props.currentUser ?? null);
   useCurrentUser(props.currentUser ?? null);
+  useIsCustomizedLogoUploaded(props.isCustomizedLogoUploaded);
 
 
   const componentTitle = t('customize_settings.customize_settings');
   const componentTitle = t('customize_settings.customize_settings');
   const pageTitle = generateCustomTitle(props, componentTitle);
   const pageTitle = generateCustomTitle(props, componentTitle);
@@ -57,6 +59,7 @@ const injectServerConfigurations = async(context: GetServerSidePropsContext, pro
   const { crowi } = req;
   const { crowi } = req;
 
 
   props.customizeTitle = crowi.configManager.getConfig('crowi', 'customize:title');
   props.customizeTitle = crowi.configManager.getConfig('crowi', 'customize:title');
+  props.isCustomizedLogoUploaded = await crowi.attachmentService.isBrandLogoExist();
 };
 };
 
 
 export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
 export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {

+ 5 - 4
packages/app/src/pages/utils/commons.ts

@@ -20,7 +20,7 @@ export type CommonProps = {
   growiVersion: string,
   growiVersion: string,
   isMaintenanceMode: boolean,
   isMaintenanceMode: boolean,
   redirectDestination: string | null,
   redirectDestination: string | null,
-  customizedLogoSrc?: string,
+  isDefaultLogo: boolean,
   currentUser?: IUser,
   currentUser?: IUser,
 } & Partial<SSRConfig>;
 } & Partial<SSRConfig>;
 
 
@@ -30,7 +30,7 @@ export const getServerSideCommonProps: GetServerSideProps<CommonProps> = async(c
   const req = context.req as CrowiRequest<IUserHasId & any>;
   const req = context.req as CrowiRequest<IUserHasId & any>;
   const { crowi, user } = req;
   const { crowi, user } = req;
   const {
   const {
-    appService, configManager, customizeService,
+    appService, configManager, customizeService, attachmentService,
   } = crowi;
   } = crowi;
 
 
   const url = new URL(context.resolvedUrl, 'http://example.com');
   const url = new URL(context.resolvedUrl, 'http://example.com');
@@ -45,7 +45,8 @@ export const getServerSideCommonProps: GetServerSideProps<CommonProps> = async(c
 
 
   // eslint-disable-next-line max-len, no-nested-ternary
   // eslint-disable-next-line max-len, no-nested-ternary
   const redirectDestination = !isMaintenanceMode && currentPathname === '/maintenance' ? '/' : isMaintenanceMode && !currentPathname.match('/admin/*') && !(currentPathname === '/maintenance') ? '/maintenance' : null;
   const redirectDestination = !isMaintenanceMode && currentPathname === '/maintenance' ? '/' : isMaintenanceMode && !currentPathname.match('/admin/*') && !(currentPathname === '/maintenance') ? '/maintenance' : null;
-  const isDefaultLogo = crowi.configManager.getConfig('crowi', 'customize:isDefaultLogo');
+  const isCustomizedLogoUploaded = await attachmentService.isBrandLogoExist();
+  const isDefaultLogo = crowi.configManager.getConfig('crowi', 'customize:isDefaultLogo') || !isCustomizedLogoUploaded;
 
 
   const props: CommonProps = {
   const props: CommonProps = {
     namespacesRequired: ['translation'],
     namespacesRequired: ['translation'],
@@ -59,8 +60,8 @@ export const getServerSideCommonProps: GetServerSideProps<CommonProps> = async(c
     growiVersion: crowi.version,
     growiVersion: crowi.version,
     isMaintenanceMode,
     isMaintenanceMode,
     redirectDestination,
     redirectDestination,
-    customizedLogoSrc: isDefaultLogo ? null : configManager.getConfig('crowi', 'customize:customizedLogoSrc'),
     currentUser,
     currentUser,
+    isDefaultLogo,
   };
   };
 
 
   return { props };
   return { props };

+ 8 - 0
packages/app/src/server/middlewares/certify-brand-logo.ts

@@ -0,0 +1,8 @@
+export const generateCertifyBrandLogoMiddleware = (crowi) => {
+
+  return async(req, res, next) => {
+    req.isBrandLogo = true;
+    next();
+  };
+
+};

+ 6 - 0
packages/app/src/server/middlewares/login-required.js

@@ -43,6 +43,12 @@ module.exports = (crowi, isGuestAllowed = false, fallback = null) => {
       return next();
       return next();
     }
     }
 
 
+    // Check if it is a Brand logo
+    if (req.isBrandLogo) {
+      logger.debug('Target is Brand logo');
+      return next();
+    }
+
     // is api path
     // is api path
     const baseUrl = req.baseUrl || '';
     const baseUrl = req.baseUrl || '';
     if (baseUrl.match(/^\/_api\/.+$/)) {
     if (baseUrl.match(/^\/_api\/.+$/)) {

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

@@ -241,9 +241,6 @@ schema.statics.getLocalconfig = function(crowi) {
     globalLang: crowi.configManager.getConfig('crowi', 'app:globalLang'),
     globalLang: crowi.configManager.getConfig('crowi', 'app:globalLang'),
     pageLimitationL: crowi.configManager.getConfig('crowi', 'customize:showPageLimitationL'),
     pageLimitationL: crowi.configManager.getConfig('crowi', 'customize:showPageLimitationL'),
     pageLimitationXL: crowi.configManager.getConfig('crowi', 'customize:showPageLimitationXL'),
     pageLimitationXL: crowi.configManager.getConfig('crowi', 'customize:showPageLimitationXL'),
-    customizedLogoSrc: isDefaultLogo != null && !isDefaultLogo
-      ? crowi.configManager.getConfig('crowi', 'customize:customizedLogoSrc')
-      : null,
     auditLogEnabled: crowi.configManager.getConfig('crowi', 'app:auditLogEnabled'),
     auditLogEnabled: crowi.configManager.getConfig('crowi', 'app:auditLogEnabled'),
     activityExpirationSeconds: crowi.configManager.getConfig('crowi', 'app:activityExpirationSeconds'),
     activityExpirationSeconds: crowi.configManager.getConfig('crowi', 'app:activityExpirationSeconds'),
     auditLogAvailableActions: crowi.activityService.getAvailableActions(false),
     auditLogAvailableActions: crowi.activityService.getAvailableActions(false),

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

@@ -660,12 +660,6 @@ module.exports = (crowi) => {
     }
     }
   });
   });
 
 
-  router.get('/customize-logo', loginRequiredStrictly, adminRequired, async(req, res) => {
-    const isDefaultLogo = await crowi.configManager.getConfig('crowi', 'customize:isDefaultLogo');
-    const customizedLogoSrc = await crowi.configManager.getConfig('crowi', 'customize:customizedLogoSrc');
-    return res.apiv3({ isDefaultLogo, customizedLogoSrc });
-  });
-
   router.put('/customize-logo', loginRequiredStrictly, adminRequired, validator.logo, apiV3FormValidator, async(req, res) => {
   router.put('/customize-logo', loginRequiredStrictly, adminRequired, validator.logo, apiV3FormValidator, async(req, res) => {
 
 
     const {
     const {
@@ -717,11 +711,6 @@ module.exports = (crowi) => {
       let attachment;
       let attachment;
       try {
       try {
         attachment = await attachmentService.createAttachment(file, req.user, null, AttachmentType.BRAND_LOGO);
         attachment = await attachmentService.createAttachment(file, req.user, null, AttachmentType.BRAND_LOGO);
-        const attachmentConfigParams = {
-          'customize:customizedLogoSrc': attachment.filePathProxied,
-        };
-
-        await crowi.configManager.updateConfigsInTheSameNamespace('crowi', attachmentConfigParams);
       }
       }
       catch (err) {
       catch (err) {
         logger.error(err);
         logger.error(err);
@@ -741,9 +730,6 @@ module.exports = (crowi) => {
 
 
     try {
     try {
       await attachmentService.removeAllAttachments(attachments);
       await attachmentService.removeAllAttachments(attachments);
-      // update attachmentId immediately
-      const attachmentConfigParams = { 'customize:customizedLogoSrc': null };
-      await crowi.configManager.updateConfigsInTheSameNamespace('crowi', attachmentConfigParams);
     }
     }
     catch (err) {
     catch (err) {
       logger.error(err);
       logger.error(err);

+ 10 - 0
packages/app/src/server/routes/attachment.js

@@ -296,6 +296,16 @@ module.exports = function(crowi, app) {
     return responseForAttachment(req, res, attachment);
     return responseForAttachment(req, res, attachment);
   };
   };
 
 
+  api.getBrandLogo = async function(req, res) {
+    const brandLogoAttachment = await Attachment.findOne({ attachmentType: AttachmentType.BRAND_LOGO });
+
+    if (brandLogoAttachment == null) {
+      return res.status(404).json(ApiResponse.error('Brand logo does not exist'));
+    }
+
+    return responseForAttachment(req, res, brandLogoAttachment);
+  };
+
   /**
   /**
    * @api {get} /attachments.obsoletedGetForMongoDB get attachments from mongoDB
    * @api {get} /attachments.obsoletedGetForMongoDB get attachments from mongoDB
    * @apiName get
    * @apiName get

+ 4 - 0
packages/app/src/server/routes/index.js

@@ -3,6 +3,7 @@ import express from 'express';
 
 
 import { generateAddActivityMiddleware } from '../middlewares/add-activity';
 import { generateAddActivityMiddleware } from '../middlewares/add-activity';
 import apiV1FormValidator from '../middlewares/apiv1-form-validator';
 import apiV1FormValidator from '../middlewares/apiv1-form-validator';
+import { generateCertifyBrandLogoMiddleware } from '../middlewares/certify-brand-logo';
 import injectResetOrderByTokenMiddleware from '../middlewares/inject-reset-order-by-token-middleware';
 import injectResetOrderByTokenMiddleware from '../middlewares/inject-reset-order-by-token-middleware';
 import injectUserRegistrationOrderByTokenMiddleware from '../middlewares/inject-user-registration-order-by-token-middleware';
 import injectUserRegistrationOrderByTokenMiddleware from '../middlewares/inject-user-registration-order-by-token-middleware';
 import * as loginFormValidator from '../middlewares/login-form-validator';
 import * as loginFormValidator from '../middlewares/login-form-validator';
@@ -30,6 +31,7 @@ module.exports = function(crowi, app) {
   const loginRequired = require('../middlewares/login-required')(crowi, true);
   const loginRequired = require('../middlewares/login-required')(crowi, true);
   const adminRequired = require('../middlewares/admin-required')(crowi);
   const adminRequired = require('../middlewares/admin-required')(crowi);
   const certifySharedFile = require('../middlewares/certify-shared-file')(crowi);
   const certifySharedFile = require('../middlewares/certify-shared-file')(crowi);
+  const certifyBrandLogo = generateCertifyBrandLogoMiddleware(crowi);
   const rateLimiter = require('../middlewares/rate-limiter')();
   const rateLimiter = require('../middlewares/rate-limiter')();
   const addActivity = generateAddActivityMiddleware(crowi);
   const addActivity = generateAddActivityMiddleware(crowi);
 
 
@@ -106,6 +108,8 @@ module.exports = function(crowi, app) {
   app.post('/_api/admin/import/qiita'           , loginRequiredStrictly , adminRequired , csrfProtection, addActivity, admin.api.importDataFromQiita);
   app.post('/_api/admin/import/qiita'           , loginRequiredStrictly , adminRequired , csrfProtection, addActivity, admin.api.importDataFromQiita);
   app.post('/_api/admin/import/testQiitaAPI'    , loginRequiredStrictly , adminRequired , csrfProtection, addActivity, admin.api.testQiitaAPI);
   app.post('/_api/admin/import/testQiitaAPI'    , loginRequiredStrictly , adminRequired , csrfProtection, addActivity, admin.api.testQiitaAPI);
 
 
+  app.get('/attachment/brand-logo' , certifyBrandLogo, loginRequired, attachment.api.getBrandLogo);
+
   /*
   /*
    * Routes below are unavailable when maintenance mode
    * Routes below are unavailable when maintenance mode
    */
    */

+ 11 - 0
packages/app/src/server/service/attachment.js

@@ -1,5 +1,7 @@
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
+import { AttachmentType } from '../interfaces/attachment';
+
 const fs = require('fs');
 const fs = require('fs');
 
 
 const mongoose = require('mongoose');
 const mongoose = require('mongoose');
@@ -77,6 +79,15 @@ class AttachmentService {
     return;
     return;
   }
   }
 
 
+  async isBrandLogoExist() {
+    const Attachment = this.crowi.model('Attachment');
+
+    const query = { attachmentType: AttachmentType.BRAND_LOGO };
+    const count = await Attachment.countDocuments(query);
+
+    return count >= 1;
+  }
+
 }
 }
 
 
 module.exports = AttachmentService;
 module.exports = AttachmentService;

+ 1 - 1
packages/app/src/services/renderer/rehype-plugins/add-line-number-attribute.ts

@@ -5,7 +5,7 @@ import { visit } from 'unist-util-visit';
 
 
 import { addClassToProperties } from './add-class';
 import { addClassToProperties } from './add-class';
 
 
-const REGEXP_TARGET_TAGNAMES = new RegExp(/^(h1|h2|h3|h4|h5|h6|p|img|pre|blockquote|hr|ol|ul)$/);
+const REGEXP_TARGET_TAGNAMES = new RegExp(/^(h1|h2|h3|h4|h5|h6|p|img|pre|blockquote|hr|ol|ul|table|tr)$/);
 
 
 export const rehypePlugin: Plugin = () => {
 export const rehypePlugin: Plugin = () => {
   return (tree) => {
   return (tree) => {

+ 5 - 2
packages/app/src/services/renderer/remark-plugins/plantuml.ts

@@ -1,12 +1,15 @@
 import plantuml from '@akebifiky/remark-simple-plantuml';
 import plantuml from '@akebifiky/remark-simple-plantuml';
 import { Plugin } from 'unified';
 import { Plugin } from 'unified';
+import urljoin from 'url-join';
 
 
 type PlantUMLPluginParams = {
 type PlantUMLPluginParams = {
-  baseUrl?: string,
+  plantumlUri?: string,
 }
 }
 
 
 export const remarkPlugin: Plugin<[PlantUMLPluginParams]> = (options) => {
 export const remarkPlugin: Plugin<[PlantUMLPluginParams]> = (options) => {
-  const baseUrl = options.baseUrl ?? 'https://www.plantuml.com/plantuml/svg';
+  const plantumlUri = options.plantumlUri ?? 'https://www.plantuml.com/plantuml';
+
+  const baseUrl = urljoin(plantumlUri, '/svg');
 
 
   return plantuml.bind(this)({ baseUrl });
   return plantuml.bind(this)({ baseUrl });
 };
 };

+ 3 - 3
packages/app/src/services/renderer/renderer.tsx

@@ -139,7 +139,7 @@ export const generateViewOptions = (
   // add remark plugins
   // add remark plugins
   remarkPlugins.push(
   remarkPlugins.push(
     math,
     math,
-    [plantuml.remarkPlugin, { baseUrl: config.plantumlUri }],
+    [plantuml.remarkPlugin, { plantumlUri: config.plantumlUri }],
     drawioPlugin.remarkPlugin,
     drawioPlugin.remarkPlugin,
     xsvToTable.remarkPlugin,
     xsvToTable.remarkPlugin,
     lsxGrowiPlugin.remarkPlugin,
     lsxGrowiPlugin.remarkPlugin,
@@ -221,7 +221,7 @@ export const generateSimpleViewOptions = (
   // add remark plugins
   // add remark plugins
   remarkPlugins.push(
   remarkPlugins.push(
     math,
     math,
-    [plantuml.remarkPlugin, { baseUrl: config.plantumlUri }],
+    [plantuml.remarkPlugin, { plantumlUri: config.plantumlUri }],
     drawioPlugin.remarkPlugin,
     drawioPlugin.remarkPlugin,
     xsvToTable.remarkPlugin,
     xsvToTable.remarkPlugin,
     lsxGrowiPlugin.remarkPlugin,
     lsxGrowiPlugin.remarkPlugin,
@@ -271,7 +271,7 @@ export const generatePreviewOptions = (config: RendererConfig, pagePath: string)
   // add remark plugins
   // add remark plugins
   remarkPlugins.push(
   remarkPlugins.push(
     math,
     math,
-    [plantuml.remarkPlugin, { baseUrl: config.plantumlUri }],
+    [plantuml.remarkPlugin, { plantumlUri: config.plantumlUri }],
     drawioPlugin.remarkPlugin,
     drawioPlugin.remarkPlugin,
     xsvToTable.remarkPlugin,
     xsvToTable.remarkPlugin,
     lsxGrowiPlugin.remarkPlugin,
     lsxGrowiPlugin.remarkPlugin,

+ 6 - 2
packages/app/src/stores/context.tsx

@@ -201,8 +201,12 @@ export const useCustomizeTitle = (initialData?: string): SWRResponse<string, Err
   return useContextSWR('CustomizeTitle', initialData);
   return useContextSWR('CustomizeTitle', initialData);
 };
 };
 
 
-export const useCustomizedLogoSrc = (initialData?: string): SWRResponse<string, Error> => {
-  return useContextSWR('customizedLogoSrc', initialData);
+export const useIsDefaultLogo = (initialData?: boolean): SWRResponse<boolean, Error> => {
+  return useContextSWR('isDefaultLogo', initialData);
+};
+
+export const useIsCustomizedLogoUploaded = (initialData?: boolean): SWRResponse<boolean, Error> => {
+  return useStaticSWR('isCustomizedLogoUploaded', initialData);
 };
 };
 
 
 export const useGrowiCloudUri = (initialData?: string): SWRResponse<string, Error> => {
 export const useGrowiCloudUri = (initialData?: string): SWRResponse<string, Error> => {