Yuki Takei 3 лет назад
Родитель
Сommit
9f5884bfc2

+ 5 - 7
packages/app/src/components/Admin/Customize/CustomizeThemeOptions.jsx

@@ -49,14 +49,13 @@ const uniqueTheme = [{
 
 
 const CustomizeThemeOptions = (props) => {
 const CustomizeThemeOptions = (props) => {
 
 
-  const { adminCustomizeContainer, currentTheme } = props;
-  const { currentLayout } = adminCustomizeContainer.state;
+  const { selectedTheme } = props;
 
 
   const { t } = useTranslation('admin');
   const { t } = useTranslation('admin');
 
 
 
 
   return (
   return (
-    <div id="themeOptions" className={`${currentLayout === 'kibela' && 'disabled'}`}>
+    <div id="themeOptions">
       {/* Light and Dark Themes */}
       {/* Light and Dark Themes */}
       <div>
       <div>
         <h3>{t('customize_settings.theme_desc.light_and_dark')}</h3>
         <h3>{t('customize_settings.theme_desc.light_and_dark')}</h3>
@@ -65,7 +64,7 @@ const CustomizeThemeOptions = (props) => {
             return (
             return (
               <ThemeColorBox
               <ThemeColorBox
                 key={theme.name}
                 key={theme.name}
-                isSelected={currentTheme === theme.name}
+                isSelected={selectedTheme === theme.name}
                 onSelected={() => props.onSelected(theme.name)}
                 onSelected={() => props.onSelected(theme.name)}
                 {...theme}
                 {...theme}
               />
               />
@@ -81,7 +80,7 @@ const CustomizeThemeOptions = (props) => {
             return (
             return (
               <ThemeColorBox
               <ThemeColorBox
                 key={theme.name}
                 key={theme.name}
-                isSelected={currentTheme === theme.name}
+                isSelected={selectedTheme === theme.name}
                 onSelected={() => props.onSelected(theme.name)}
                 onSelected={() => props.onSelected(theme.name)}
                 {...theme}
                 {...theme}
               />
               />
@@ -97,9 +96,8 @@ const CustomizeThemeOptions = (props) => {
 const CustomizeThemeOptionsWrapper = withUnstatedContainers(CustomizeThemeOptions, [AdminCustomizeContainer]);
 const CustomizeThemeOptionsWrapper = withUnstatedContainers(CustomizeThemeOptions, [AdminCustomizeContainer]);
 
 
 CustomizeThemeOptions.propTypes = {
 CustomizeThemeOptions.propTypes = {
-  adminCustomizeContainer: PropTypes.instanceOf(AdminCustomizeContainer).isRequired,
   onSelected: PropTypes.func,
   onSelected: PropTypes.func,
-  currentTheme: PropTypes.string,
+  selectedTheme: PropTypes.string,
 };
 };
 
 
 export default CustomizeThemeOptionsWrapper;
 export default CustomizeThemeOptionsWrapper;

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

@@ -1,60 +1,61 @@
-import React, { useCallback } from 'react';
+import React, { useCallback, useEffect, useState } from 'react';
 
 
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
-import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
-import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { apiv3Put } from '~/client/util/apiv3-client';
 import { apiv3Put } from '~/client/util/apiv3-client';
-import { useGrowiTheme } from '~/stores/context';
+import { toastSuccess, toastError, toastWarning } from '~/client/util/toastr';
+import { useSWRxGrowiTheme } from '~/stores/admin/customize';
 
 
-
-import { withUnstatedContainers } from '../../UnstatedUtils';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 
 
 import CustomizeThemeOptions from './CustomizeThemeOptions';
 import CustomizeThemeOptions from './CustomizeThemeOptions';
 
 
+// eslint-disable-next-line @typescript-eslint/ban-types
 type Props = {
 type Props = {
-  adminCustomizeContainer: AdminCustomizeContainer
 }
 }
 
 
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
 const CustomizeThemeSetting = (props: Props): JSX.Element => {
 const CustomizeThemeSetting = (props: Props): JSX.Element => {
-
-  const { adminCustomizeContainer } = props;
-  const { data: currentTheme } = useGrowiTheme();
   const { t } = useTranslation();
   const { t } = useTranslation();
 
 
-  const selectedHandler = useCallback((themeName) => {
-    // TODO: preview without using mutate of useGrowiTheme
-    // https://github.com/weseek/growi/pull/6860
-    // mutateGrowiTheme(themeName);
+  const { data: currentTheme, error } = useSWRxGrowiTheme();
+  const [selectedTheme, setSelectedTheme] = useState(currentTheme);
+
+  useEffect(() => {
+    setSelectedTheme(currentTheme);
+  }, [currentTheme]);
+
+  const selectedHandler = useCallback((themeName: string) => {
+    setSelectedTheme(themeName);
   }, []);
   }, []);
 
 
   const submitHandler = useCallback(async() => {
   const submitHandler = useCallback(async() => {
+    if (selectedTheme == null) {
+      toastWarning('The selected theme is undefined');
+      return;
+    }
+
     try {
     try {
-      if (currentTheme != null) {
-        await apiv3Put('/customize-setting/theme', {
-          themeType: currentTheme,
-        });
-      }
+      await apiv3Put('/customize-setting/theme', {
+        theme: selectedTheme,
+      });
 
 
       toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.theme'), ns: 'commons' }));
       toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.theme'), ns: 'commons' }));
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);
     }
     }
-  }, [currentTheme, t]);
+  }, [selectedTheme, t]);
 
 
   return (
   return (
     <div className="row">
     <div className="row">
       <div className="col-12">
       <div className="col-12">
         <h2 className="admin-setting-header">{t('admin:customize_settings.theme')}</h2>
         <h2 className="admin-setting-header">{t('admin:customize_settings.theme')}</h2>
-        <CustomizeThemeOptions onSelected={selectedHandler} currentTheme={currentTheme} />
-        <AdminUpdateButtonRow onClick={submitHandler} disabled={adminCustomizeContainer.state.retrieveError != null} />
+        <CustomizeThemeOptions onSelected={selectedHandler} selectedTheme={selectedTheme} />
+        <AdminUpdateButtonRow onClick={submitHandler} disabled={error != null} />
       </div>
       </div>
     </div>
     </div>
   );
   );
 };
 };
 
 
-const CustomizeThemeSettingWrapper = withUnstatedContainers(CustomizeThemeSetting, [AdminCustomizeContainer]);
-
-export default CustomizeThemeSettingWrapper;
+export default CustomizeThemeSetting;

+ 0 - 3
packages/app/src/components/Layout/RawLayout.tsx

@@ -4,7 +4,6 @@ import Head from 'next/head';
 import { ToastContainer } from 'react-toastify';
 import { ToastContainer } from 'react-toastify';
 import { useIsomorphicLayoutEffect } from 'usehooks-ts';
 import { useIsomorphicLayoutEffect } from 'usehooks-ts';
 
 
-import { useGrowiTheme } from '~/stores/context';
 import { ColorScheme, useNextThemes, NextThemesProvider } from '~/stores/use-next-themes';
 import { ColorScheme, useNextThemes, NextThemesProvider } from '~/stores/use-next-themes';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
@@ -23,8 +22,6 @@ export const RawLayout = ({ children, title, className }: Props): JSX.Element =>
   if (className != null) {
   if (className != null) {
     classNames.push(className);
     classNames.push(className);
   }
   }
-  const { data: growiTheme } = useGrowiTheme();
-
   // get color scheme from next-themes
   // get color scheme from next-themes
   const { resolvedTheme, resolvedThemeByAttributes } = useNextThemes();
   const { resolvedTheme, resolvedThemeByAttributes } = useNextThemes();
 
 

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

@@ -1,3 +1,7 @@
 export type IResLayoutSetting = {
 export type IResLayoutSetting = {
   isContainerFluid: boolean,
   isContainerFluid: boolean,
 };
 };
+
+export type IResGrowiTheme = {
+  theme: string,
+}

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

@@ -10,7 +10,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, useGrowiTheme, useGrowiVersion, useSiteUrl, useCustomizedLogoSrc,
+  useAppTitle, useConfidential, useGrowiVersion, useSiteUrl, useCustomizedLogoSrc,
 } from '~/stores/context';
 } from '~/stores/context';
 import { SWRConfigValue, swrGlobalConfiguration } from '~/utils/swr-utils';
 import { SWRConfigValue, swrGlobalConfiguration } from '~/utils/swr-utils';
 
 
@@ -55,7 +55,6 @@ function GrowiApp({ Component, pageProps }: GrowiAppProps): JSX.Element {
   useAppTitle(commonPageProps.appTitle);
   useAppTitle(commonPageProps.appTitle);
   useSiteUrl(commonPageProps.siteUrl);
   useSiteUrl(commonPageProps.siteUrl);
   useConfidential(commonPageProps.confidential);
   useConfidential(commonPageProps.confidential);
-  useGrowiTheme(commonPageProps.theme);
   useGrowiVersion(commonPageProps.growiVersion);
   useGrowiVersion(commonPageProps.growiVersion);
   useCustomizedLogoSrc(commonPageProps.customizedLogoSrc);
   useCustomizedLogoSrc(commonPageProps.customizedLogoSrc);
 
 

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

@@ -7,7 +7,6 @@ import { SSRConfig, UserConfig } from 'next-i18next';
 import * as nextI18NextConfig from '^/config/next-i18next.config';
 import * as nextI18NextConfig from '^/config/next-i18next.config';
 
 
 import { CrowiRequest } from '~/interfaces/crowi-request';
 import { CrowiRequest } from '~/interfaces/crowi-request';
-import { GrowiThemes } from '~/interfaces/theme';
 
 
 export type CommonProps = {
 export type CommonProps = {
   namespacesRequired: string[], // i18next
   namespacesRequired: string[], // i18next
@@ -15,7 +14,6 @@ export type CommonProps = {
   appTitle: string,
   appTitle: string,
   siteUrl: string,
   siteUrl: string,
   confidential: string,
   confidential: string,
-  theme: GrowiThemes,
   customTitleTemplate: string,
   customTitleTemplate: string,
   csrfToken: string,
   csrfToken: string,
   isContainerFluid: boolean,
   isContainerFluid: boolean,
@@ -55,7 +53,6 @@ export const getServerSideCommonProps: GetServerSideProps<CommonProps> = async(c
     appTitle: appService.getAppTitle(),
     appTitle: appService.getAppTitle(),
     siteUrl: configManager.getConfig('crowi', 'app:siteUrl'), // DON'T USE appService.getSiteUrl()
     siteUrl: configManager.getConfig('crowi', 'app:siteUrl'), // DON'T USE appService.getSiteUrl()
     confidential: appService.getAppConfidential() || '',
     confidential: appService.getAppConfidential() || '',
-    theme: configManager.getConfig('crowi', 'customize:theme'),
     customTitleTemplate: customizeService.customTitleTemplate,
     customTitleTemplate: customizeService.customTitleTemplate,
     csrfToken: req.csrfToken(),
     csrfToken: req.csrfToken(),
     isContainerFluid: configManager.getConfig('crowi', 'customize:isContainerFluid') ?? false,
     isContainerFluid: configManager.getConfig('crowi', 'customize:isContainerFluid') ?? false,

+ 12 - 40
packages/app/src/server/routes/apiv3/customize-setting.js

@@ -108,11 +108,8 @@ module.exports = (crowi) => {
     layout: [
     layout: [
       body('isContainerFluid').isBoolean(),
       body('isContainerFluid').isBoolean(),
     ],
     ],
-    themeAssetPath: [
-      query('themeName').isString(),
-    ],
     theme: [
     theme: [
-      body('themeType').isString(),
+      body('theme').isString(),
     ],
     ],
     sidebar: [
     sidebar: [
       body('isSidebarDrawerMode').isBoolean(),
       body('isSidebarDrawerMode').isBoolean(),
@@ -175,7 +172,6 @@ module.exports = (crowi) => {
    */
    */
   router.get('/', loginRequiredStrictly, adminRequired, async(req, res) => {
   router.get('/', loginRequiredStrictly, adminRequired, async(req, res) => {
     const customizeParams = {
     const customizeParams = {
-      themeType: await crowi.configManager.getConfig('crowi', 'customize:theme'),
       isEnabledTimeline: await crowi.configManager.getConfig('crowi', 'customize:isEnabledTimeline'),
       isEnabledTimeline: await crowi.configManager.getConfig('crowi', 'customize:isEnabledTimeline'),
       isEnabledAttachTitleHeader: await crowi.configManager.getConfig('crowi', 'customize:isEnabledAttachTitleHeader'),
       isEnabledAttachTitleHeader: await crowi.configManager.getConfig('crowi', 'customize:isEnabledAttachTitleHeader'),
       pageLimitationS: await crowi.configManager.getConfig('crowi', 'customize:showPageLimitationS'),
       pageLimitationS: await crowi.configManager.getConfig('crowi', 'customize:showPageLimitationS'),
@@ -272,41 +268,17 @@ module.exports = (crowi) => {
     }
     }
   });
   });
 
 
-  /**
-   * @swagger
-   *
-   *    /customize-setting/theme/asset-path:
-   *      put:
-   *        tags: [CustomizeSetting]
-   *        operationId: getThemeAssetPath
-   *        summary: /customize-setting/theme/asset-path
-   *        description: Get theme asset path
-   *        parameters:
-   *          - name: themeName
-   *            in: query
-   *            required: true
-   *            schema:
-   *              type: string
-   *        responses:
-   *          200:
-   *            description: Succeeded to get theme asset path
-   *            content:
-   *              application/json:
-   *                schema:
-   *                  properties:
-   *                    assetPath:
-   *                      type: string
-   */
-  router.get('/theme/asset-path', loginRequiredStrictly, adminRequired, validator.themeAssetPath, apiV3FormValidator, async(req, res) => {
-    const { themeName } = req.query;
-
-    const webpackAssetKey = `styles/theme-${themeName}.css`;
-    const assetPath = res.locals.webpack_asset(webpackAssetKey);
+  router.get('/theme', loginRequiredStrictly, adminRequired, async(req, res) => {
 
 
-    if (assetPath == null) {
-      return res.apiv3Err(new ErrorV3(`The asset for '${webpackAssetKey}' is undefined.`, 'invalid-asset'));
+    try {
+      const theme = await crowi.configManager.getConfig('crowi', 'customize:theme');
+      return res.apiv3({ theme });
+    }
+    catch (err) {
+      const msg = 'Error occurred in getting theme';
+      logger.error('Error', err);
+      return res.apiv3Err(new ErrorV3(msg, 'get-theme-failed'));
     }
     }
-    return res.apiv3({ assetPath });
   });
   });
 
 
   /**
   /**
@@ -334,13 +306,13 @@ module.exports = (crowi) => {
    */
    */
   router.put('/theme', loginRequiredStrictly, adminRequired, addActivity, validator.theme, apiV3FormValidator, async(req, res) => {
   router.put('/theme', loginRequiredStrictly, adminRequired, addActivity, validator.theme, apiV3FormValidator, async(req, res) => {
     const requestParams = {
     const requestParams = {
-      'customize:theme': req.body.themeType,
+      'customize:theme': req.body.theme,
     };
     };
 
 
     try {
     try {
       await crowi.configManager.updateConfigsInTheSameNamespace('crowi', requestParams);
       await crowi.configManager.updateConfigsInTheSameNamespace('crowi', requestParams);
       const customizedParams = {
       const customizedParams = {
-        themeType: await crowi.configManager.getConfig('crowi', 'customize:theme'),
+        theme: await crowi.configManager.getConfig('crowi', 'customize:theme'),
       };
       };
       const parameters = { action: SupportedAction.ACTION_ADMIN_THEME_UPDATE };
       const parameters = { action: SupportedAction.ACTION_ADMIN_THEME_UPDATE };
       activityEvent.emit('update', res.locals.activity._id, parameters);
       activityEvent.emit('update', res.locals.activity._id, parameters);

+ 31 - 4
packages/app/src/stores/admin/customize.tsx

@@ -1,10 +1,11 @@
 import { useCallback } from 'react';
 import { useCallback } from 'react';
 
 
-import useSWR, { SWRResponse } from 'swr';
+import { SWRResponse } from 'swr';
+import useSWRImmutable from 'swr/immutable';
 
 
 import { apiv3Get, apiv3Put } from '~/client/util/apiv3-client';
 import { apiv3Get, apiv3Put } from '~/client/util/apiv3-client';
-import { updateConfigMethodForAdmin } from '~/interfaces/admin';
-import { IResLayoutSetting } from '~/interfaces/customize';
+import type { updateConfigMethodForAdmin } from '~/interfaces/admin';
+import type { IResLayoutSetting, IResGrowiTheme } from '~/interfaces/customize';
 
 
 export const useSWRxLayoutSetting = (): SWRResponse<IResLayoutSetting, Error> & updateConfigMethodForAdmin<IResLayoutSetting> => {
 export const useSWRxLayoutSetting = (): SWRResponse<IResLayoutSetting, Error> & updateConfigMethodForAdmin<IResLayoutSetting> => {
 
 
@@ -13,7 +14,7 @@ export const useSWRxLayoutSetting = (): SWRResponse<IResLayoutSetting, Error> &
     return res.data;
     return res.data;
   }, []);
   }, []);
 
 
-  const swrResponse = useSWR('/customize-setting/layout', fetcher);
+  const swrResponse = useSWRImmutable('/customize-setting/layout', fetcher);
 
 
   const update = useCallback(async(layoutSetting: IResLayoutSetting) => {
   const update = useCallback(async(layoutSetting: IResLayoutSetting) => {
     await apiv3Put('/customize-setting/layout', layoutSetting);
     await apiv3Put('/customize-setting/layout', layoutSetting);
@@ -25,3 +26,29 @@ export const useSWRxLayoutSetting = (): SWRResponse<IResLayoutSetting, Error> &
     update,
     update,
   };
   };
 };
 };
+
+export const useSWRxGrowiTheme = (): SWRResponse<string, Error> => {
+
+  const fetcher = useCallback(async() => {
+    const res = await apiv3Get<IResGrowiTheme>('/customize-setting/theme');
+    return res.data.theme;
+  }, []);
+
+  const swrResponse = useSWRImmutable('/customize-setting/theme', fetcher);
+
+  const update = async(theme: string) => {
+    await apiv3Put('/customize-setting/layout', { theme });
+    await swrResponse.mutate();
+    // The updateFn should be a promise or asynchronous function to handle the remote mutation
+    // it should return updated data. see: https://swr.vercel.app/docs/mutation#optimistic-updates
+    // Moreover, `async() => false` does not work since it's too fast to be calculated.
+    await swrResponse.mutate(new Promise(r => setTimeout(() => r(theme), 10)), { optimisticData: () => theme });
+  };
+
+  return Object.assign(
+    swrResponse,
+    {
+      update,
+    },
+  );
+};

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

@@ -37,10 +37,6 @@ export const useConfidential = (initialData?: string): SWRResponse<string, Error
   return useContextSWR('confidential', initialData);
   return useContextSWR('confidential', initialData);
 };
 };
 
 
-export const useGrowiTheme = (initialData?: GrowiThemes): SWRResponse<GrowiThemes, Error> => {
-  return useContextSWR('theme', initialData);
-};
-
 export const useCurrentUser = (initialData?: Nullable<IUser>): SWRResponse<Nullable<IUser>, Error> => {
 export const useCurrentUser = (initialData?: Nullable<IUser>): SWRResponse<Nullable<IUser>, Error> => {
   return useContextSWR<Nullable<IUser>, Error>('currentUser', initialData);
   return useContextSWR<Nullable<IUser>, Error>('currentUser', initialData);
 };
 };