Shun Miyazawa 3 년 전
부모
커밋
47619d6189

+ 20 - 34
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 [isDefaultLogo, setIsDefaultLogo] = 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 selectedCurrentLogo = useMemo(() => {
+    return isDefaultLogo || !isCustomizedLogoUploaded ? DEFAULT_LOGO : CUSTOMIZED_LOGO;
+  }, [isDefaultLogo, isCustomizedLogoUploaded]);
 
 
   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,11 +41,7 @@ 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 });
       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) {
@@ -67,7 +52,7 @@ const CustomizeLogoSetting = (): JSX.Element => {
   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>
@@ -117,6 +102,7 @@ const CustomizeLogoSetting = (): JSX.Element => {
                     </label>
                     </label>
                   </div>
                   </div>
                 </h4>
                 </h4>
+                {/* eslint-disable-next-line @next/next/no-img-element */}
                 <img src={DEFAULT_LOGO} width="64" />
                 <img src={DEFAULT_LOGO} width="64" />
               </div>
               </div>
               <div className="col-md-6 col-12">
               <div className="col-md-6 col-12">
@@ -141,8 +127,8 @@ 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) && (
+                    <p><img src={selectedCurrentLogo} className="picture picture-lg " id="settingBrandLogo" width="64" /></p>
+                    {isCustomizedLogoUploaded && (
                       <button type="button" className="btn btn-danger" onClick={onClickDeleteBtn}>
                       <button type="button" className="btn btn-danger" onClick={onClickDeleteBtn}>
                         { t('admin:customize_settings.delete_logo') }
                         { t('admin:customize_settings.delete_logo') }
                       </button>
                       </button>

+ 10 - 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, useIsCustomizedLogoUploaded,
 } 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,17 @@ const Confidential: FC<ConfidentialProps> = memo((props: ConfidentialProps): JSX
 Confidential.displayName = 'Confidential';
 Confidential.displayName = 'Confidential';
 
 
 interface NavbarLogoProps {
 interface NavbarLogoProps {
-  logoSrc?: string,
+  isDefaultLogo?: boolean
+  isCustomizedLogoUploaded?: boolean
 }
 }
 
 
 const GrowiNavbarLogo: FC<NavbarLogoProps> = memo((props: NavbarLogoProps) => {
 const GrowiNavbarLogo: FC<NavbarLogoProps> = memo((props: NavbarLogoProps) => {
-  const { logoSrc } = props;
+  const { isDefaultLogo, isCustomizedLogoUploaded } = props;
 
 
-  return logoSrc != null
+  return isDefaultLogo || !isCustomizedLogoUploaded
+    ? <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 +152,8 @@ 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();
+  const { data: isCustomizedLogoUploaded } = useIsCustomizedLogoUploaded();
 
 
   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 +161,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} isCustomizedLogoUploaded={isCustomizedLogoUploaded}/>
           </a>
           </a>
         </Link>
         </Link>
       </div>
       </div>

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

+ 3 - 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, useIsCustomizedLogoUploaded,
 } from '~/stores/context';
 } from '~/stores/context';
 import { SWRConfigValue, swrGlobalConfiguration } from '~/utils/swr-utils';
 import { SWRConfigValue, swrGlobalConfiguration } from '~/utils/swr-utils';
 
 
@@ -63,7 +63,8 @@ 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);
+  useIsCustomizedLogoUploaded(commonPageProps.isCustomizedLogoUploaded);
 
 
   // 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);

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

@@ -20,7 +20,8 @@ export type CommonProps = {
   growiVersion: string,
   growiVersion: string,
   isMaintenanceMode: boolean,
   isMaintenanceMode: boolean,
   redirectDestination: string | null,
   redirectDestination: string | null,
-  customizedLogoSrc?: string,
+  isDefaultLogo: boolean,
+  isCustomizedLogoUploaded: boolean,
   currentUser?: IUser,
   currentUser?: IUser,
 } & Partial<SSRConfig>;
 } & Partial<SSRConfig>;
 
 
@@ -30,7 +31,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');
@@ -46,6 +47,7 @@ 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 isDefaultLogo = crowi.configManager.getConfig('crowi', 'customize:isDefaultLogo');
+  const isCustomizedLogoUploaded = await attachmentService.isBrandLogoExist();
 
 
   const props: CommonProps = {
   const props: CommonProps = {
     namespacesRequired: ['translation'],
     namespacesRequired: ['translation'],
@@ -59,8 +61,9 @@ 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,
+    isCustomizedLogoUploaded,
   };
   };
 
 
   return { props };
   return { props };

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

+ 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> => {