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

Merge pull request #7237 from weseek/imprv/force-color-scheme

imprv: Force color scheme by growi theme
Yuki Takei 3 лет назад
Родитель
Сommit
d102d14773

+ 3 - 3
packages/app/src/components/Admin/Customize/CustomizeThemeOptions.tsx

@@ -9,7 +9,7 @@ import { ThemeColorBox } from './ThemeColorBox';
 type Props = {
   availableThemes: GrowiThemeMetadata[],
   selectedTheme?: string,
-  onSelected?: (themeName: string) => void,
+  onSelected?: (themeName: string, schemeType: GrowiThemeSchemeType) => void,
 };
 
 const CustomizeThemeOptions = (props: Props): JSX.Element => {
@@ -36,7 +36,7 @@ const CustomizeThemeOptions = (props: Props): JSX.Element => {
                 key={theme.name}
                 isSelected={selectedTheme != null && selectedTheme === theme.name}
                 metadata={theme}
-                onSelected={() => onSelected?.(theme.name)}
+                onSelected={() => onSelected?.(theme.name, theme.schemeType)}
               />
             );
           })}
@@ -52,7 +52,7 @@ const CustomizeThemeOptions = (props: Props): JSX.Element => {
                 key={theme.name}
                 isSelected={selectedTheme != null && selectedTheme === theme.name}
                 metadata={theme}
-                onSelected={() => onSelected?.(theme.name)}
+                onSelected={() => onSelected?.(theme.name, theme.schemeType)}
               />
             );
           })}

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

@@ -1,9 +1,9 @@
 import React, { useCallback, useEffect, useState } from 'react';
 
+import { GrowiThemeSchemeType } from '@growi/core';
 import { PresetThemes, PresetThemesMetadatas } from '@growi/preset-themes';
 import { useTranslation } from 'next-i18next';
 
-import { apiv3Put } from '~/client/util/apiv3-client';
 import { toastSuccess, toastError, toastWarning } from '~/client/util/toastr';
 import { useSWRxGrowiThemeSetting } from '~/stores/admin/customize';
 
@@ -20,26 +20,29 @@ type Props = {
 const CustomizeThemeSetting = (props: Props): JSX.Element => {
   const { t } = useTranslation();
 
-  const { data, error } = useSWRxGrowiThemeSetting();
+  const { data, error, update } = useSWRxGrowiThemeSetting();
   const [currentTheme, setCurrentTheme] = useState(data?.currentTheme);
+  const [currentForcedColorScheme, setCurrentForcedColorScheme] = useState(data?.currentForcedColorScheme);
 
   useEffect(() => {
     setCurrentTheme(data?.currentTheme);
   }, [data?.currentTheme]);
 
-  const selectedHandler = useCallback((themeName: string) => {
+  const selectedHandler = useCallback((themeName: string, schemeType: GrowiThemeSchemeType) => {
     setCurrentTheme(themeName);
+    setCurrentForcedColorScheme(schemeType === GrowiThemeSchemeType.BOTH ? undefined : schemeType);
   }, []);
 
   const submitHandler = useCallback(async() => {
-    if (currentTheme == null) {
+    if (currentTheme == null || currentForcedColorScheme == null) {
       toastWarning('The selected theme is undefined');
       return;
     }
 
     try {
-      await apiv3Put('/customize-setting/theme', {
+      await update({
         theme: currentTheme,
+        forcedColorScheme: currentForcedColorScheme,
       });
 
       toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.theme'), ns: 'commons' }));
@@ -47,7 +50,7 @@ const CustomizeThemeSetting = (props: Props): JSX.Element => {
     catch (err) {
       toastError(err);
     }
-  }, [currentTheme, t]);
+  }, [currentForcedColorScheme, currentTheme, t, update]);
 
   const availableThemes = data?.pluginThemesMetadatas == null
     ? PresetThemesMetadatas

+ 2 - 1
packages/app/src/components/Layout/RawLayout.tsx

@@ -1,10 +1,11 @@
 import React, { ReactNode, useState } from 'react';
 
+import { ColorScheme } from '@growi/core';
 import Head from 'next/head';
 import { ToastContainer } from 'react-toastify';
 import { useIsomorphicLayoutEffect } from 'usehooks-ts';
 
-import { ColorScheme, useNextThemes, NextThemesProvider } from '~/stores/use-next-themes';
+import { useNextThemes, NextThemesProvider } from '~/stores/use-next-themes';
 import loggerFactory from '~/utils/logger';
 
 

+ 3 - 3
packages/app/src/components/Navbar/AppearanceModeDropdown.tsx

@@ -47,15 +47,15 @@ export const AppearanceModeDropdown:FC<AppearanceModeDropdownProps> = (props: Ap
 
   const followOsCheckboxModifiedHandler = useCallback((isChecked: boolean) => {
     if (isChecked) {
-      setTheme(Themes.system);
+      setTheme(Themes.SYSTEM);
     }
     else {
-      setTheme(resolvedTheme ?? Themes.light);
+      setTheme(resolvedTheme ?? Themes.LIGHT);
     }
   }, [resolvedTheme, setTheme]);
 
   const userPreferenceSwitchModifiedHandler = useCallback((isDarkMode: boolean) => {
-    setTheme(isDarkMode ? Themes.dark : Themes.light);
+    setTheme(isDarkMode ? Themes.DARK : Themes.LIGHT);
   }, [setTheme]);
 
   /* eslint-disable react/prop-types */

+ 2 - 1
packages/app/src/interfaces/customize.ts

@@ -1,4 +1,4 @@
-import { GrowiThemeMetadata } from '@growi/core';
+import type { ColorScheme, GrowiThemeMetadata } from '@growi/core';
 
 export type IResLayoutSetting = {
   isContainerFluid: boolean,
@@ -6,5 +6,6 @@ export type IResLayoutSetting = {
 
 export type IResGrowiTheme = {
   currentTheme: string,
+  currentForcedColorScheme: ColorScheme,
   pluginThemesMetadatas: GrowiThemeMetadata[],
 }

+ 2 - 1
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 { useI18nextHMR } from '~/services/i18next-hmr';
 import {
-  useAppTitle, useConfidential, useGrowiVersion, useSiteUrl, useIsDefaultLogo,
+  useAppTitle, useConfidential, useGrowiVersion, useSiteUrl, useIsDefaultLogo, useForcedColorScheme,
 } from '~/stores/context';
 import { SWRConfigValue, swrGlobalConfiguration } from '~/utils/swr-utils';
 
@@ -66,6 +66,7 @@ function GrowiApp({ Component, pageProps }: GrowiAppProps): JSX.Element {
   useConfidential(commonPageProps.confidential);
   useGrowiVersion(commonPageProps.growiVersion);
   useIsDefaultLogo(commonPageProps.isDefaultLogo);
+  useForcedColorScheme(commonPageProps.forcedColorScheme);
 
   // Use the layout defined at the page level, if available
   const getLayout = Component.getLayout ?? (page => page);

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

@@ -1,12 +1,13 @@
+import type { ColorScheme, IUser, IUserHasId } from '@growi/core';
 import {
-  DevidedPagePath, Lang, AllLang, IUser, IUserHasId,
+  DevidedPagePath, Lang, AllLang,
 } from '@growi/core';
 import { GetServerSideProps, GetServerSidePropsContext } from 'next';
 import { SSRConfig, UserConfig } from 'next-i18next';
 
 import * as nextI18NextConfig from '^/config/next-i18next.config';
 
-import { CrowiRequest } from '~/interfaces/crowi-request';
+import type { CrowiRequest } from '~/interfaces/crowi-request';
 
 export type CommonProps = {
   namespacesRequired: string[], // i18next
@@ -22,6 +23,7 @@ export type CommonProps = {
   redirectDestination: string | null,
   isDefaultLogo: boolean,
   currentUser?: IUser,
+  forcedColorScheme?: ColorScheme,
 } & Partial<SSRConfig>;
 
 // eslint-disable-next-line max-len
@@ -47,6 +49,7 @@ export const getServerSideCommonProps: GetServerSideProps<CommonProps> = async(c
   const redirectDestination = !isMaintenanceMode && currentPathname === '/maintenance' ? '/' : isMaintenanceMode && !currentPathname.match('/admin/*') && !(currentPathname === '/maintenance') ? '/maintenance' : null;
   const isCustomizedLogoUploaded = await attachmentService.isBrandLogoExist();
   const isDefaultLogo = crowi.configManager.getConfig('crowi', 'customize:isDefaultLogo') || !isCustomizedLogoUploaded;
+  const forcedColorScheme = crowi.configManager.getConfig('crowi', 'customize:theme:forcedColorScheme');
 
   const props: CommonProps = {
     namespacesRequired: ['translation'],
@@ -62,6 +65,7 @@ export const getServerSideCommonProps: GetServerSideProps<CommonProps> = async(c
     redirectDestination,
     currentUser,
     isDefaultLogo,
+    forcedColorScheme,
   };
 
   return { props };

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

@@ -116,6 +116,7 @@ export const defaultCrowiConfigs: { [key: string]: any } = {
   'customize:highlightJsStyle' : 'github',
   'customize:highlightJsStyleBorder' : false,
   'customize:theme' : PresetThemes.DEFAULT,
+  'customize:theme:forcedColorScheme' : null,
   'customize:isContainerFluid' : false,
   'customize:isEnabledTimeline' : true,
   'customize:isEnabledAttachTitleHeader' : false,

+ 5 - 1
packages/app/src/server/routes/apiv3/customize-setting.js

@@ -112,6 +112,7 @@ module.exports = (crowi) => {
     ],
     theme: [
       body('theme').isString(),
+      body('forcedColorScheme').isString().optional({ nullable: true }),
     ],
     sidebar: [
       body('isSidebarDrawerMode').isBoolean(),
@@ -274,6 +275,7 @@ module.exports = (crowi) => {
 
     try {
       const currentTheme = await crowi.configManager.getConfig('crowi', 'customize:theme');
+      const currentForcedColorScheme = await crowi.configManager.getConfig('crowi', 'customize:theme:forcedColorScheme');
 
       // retrieve plugin manifests
       const GrowiPluginModel = mongoose.model('GrowiPlugin');
@@ -283,7 +285,7 @@ module.exports = (crowi) => {
         .map(themePlugin => themePlugin.meta.themes)
         .flat();
 
-      return res.apiv3({ currentTheme, pluginThemesMetadatas });
+      return res.apiv3({ currentTheme, currentForcedColorScheme, pluginThemesMetadatas });
     }
     catch (err) {
       const msg = 'Error occurred in getting theme';
@@ -318,12 +320,14 @@ module.exports = (crowi) => {
   router.put('/theme', loginRequiredStrictly, adminRequired, addActivity, validator.theme, apiV3FormValidator, async(req, res) => {
     const requestParams = {
       'customize:theme': req.body.theme,
+      'customize:theme:forcedColorScheme': req.body.forcedColorScheme,
     };
 
     try {
       await crowi.configManager.updateConfigsInTheSameNamespace('crowi', requestParams);
       const customizedParams = {
         theme: await crowi.configManager.getConfig('crowi', 'customize:theme'),
+        forcedColorScheme: await crowi.configManager.getConfig('crowi', 'customize:theme:forcedColorScheme'),
       };
       const parameters = { action: SupportedAction.ACTION_ADMIN_THEME_UPDATE };
       activityEvent.emit('update', res.locals.activity._id, parameters);

+ 9 - 3
packages/app/src/stores/admin/customize.tsx

@@ -1,5 +1,6 @@
 import { useCallback } from 'react';
 
+import { ColorScheme } from '@growi/core';
 import useSWR, { SWRResponse } from 'swr';
 import useSWRImmutable from 'swr/immutable';
 
@@ -27,7 +28,11 @@ export const useSWRxLayoutSetting = (): SWRResponse<IResLayoutSetting, Error> &
   };
 };
 
-export const useSWRxGrowiThemeSetting = (): SWRResponse<IResGrowiTheme, Error> => {
+type UpdateThemeArgs = {
+  theme: string,
+  forcedColorScheme: ColorScheme | null,
+}
+export const useSWRxGrowiThemeSetting = (): SWRResponse<IResGrowiTheme, Error> & updateConfigMethodForAdmin<UpdateThemeArgs> => {
 
   const fetcher = useCallback(async() => {
     const res = await apiv3Get<IResGrowiTheme>('/customize-setting/theme');
@@ -36,8 +41,9 @@ export const useSWRxGrowiThemeSetting = (): SWRResponse<IResGrowiTheme, Error> =
 
   const swrResponse = useSWR('/customize-setting/theme', fetcher);
 
-  const update = async(theme: string) => {
-    await apiv3Put('/customize-setting/layout', { theme });
+  const update = async({ theme, forcedColorScheme }: UpdateThemeArgs) => {
+
+    await apiv3Put('/customize-setting/theme', { theme, forcedColorScheme });
 
     if (swrResponse.data == null) {
       swrResponse.mutate();

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

@@ -1,4 +1,4 @@
-import { IUser } from '@growi/core';
+import type { ColorScheme, IUser } from '@growi/core';
 import { Key, SWRResponse } from 'swr';
 import useSWRImmutable from 'swr/immutable';
 
@@ -209,6 +209,10 @@ export const useIsCustomizedLogoUploaded = (initialData?: boolean): SWRResponse<
   return useStaticSWR('isCustomizedLogoUploaded', initialData);
 };
 
+export const useForcedColorScheme = (initialData?: ColorScheme): SWRResponse<ColorScheme, Error> => {
+  return useContextSWR('forcedColorScheme', initialData);
+};
+
 export const useGrowiCloudUri = (initialData?: string): SWRResponse<string, Error> => {
   return useStaticSWR('growiCloudUri', initialData);
 };

+ 14 - 19
packages/app/src/stores/use-next-themes.tsx

@@ -1,35 +1,30 @@
-import { isClient } from '@growi/core';
+import { isClient, ColorScheme } from '@growi/core';
 import { ThemeProvider, useTheme } from 'next-themes';
 import { ThemeProviderProps, UseThemeProps } from 'next-themes/dist/types';
 
+import { useForcedColorScheme } from './context';
+
 export const Themes = {
-  light: 'light',
-  dark: 'dark',
-  system: 'system',
+  ...ColorScheme,
+  SYSTEM: 'system',
 } as const;
 export type Themes = typeof Themes[keyof typeof Themes];
 
-export const ResolvedThemes = {
-  light: Themes.light,
-  dark: Themes.dark,
-} as const;
-export type ResolvedThemes = typeof ResolvedThemes[keyof typeof ResolvedThemes];
-export const ColorScheme = ResolvedThemes;
-export type ColorScheme = ResolvedThemes;
-
 
 const ATTRIBUTE = 'data-theme';
 
 export const NextThemesProvider: React.FC<ThemeProviderProps> = (props) => {
-  return <ThemeProvider {...props} attribute={ATTRIBUTE} />;
+  const { data: forcedColorScheme } = useForcedColorScheme();
+
+  return <ThemeProvider {...props} forcedTheme={forcedColorScheme} attribute={ATTRIBUTE} />;
 };
 
 type UseThemeExtendedProps = Omit<UseThemeProps, 'theme'|'resolvedTheme'> & {
   theme: Themes,
-  resolvedTheme: ResolvedThemes,
+  resolvedTheme: ColorScheme,
   useOsSettings: boolean,
   isDarkMode: boolean,
-  resolvedThemeByAttributes?: ResolvedThemes,
+  resolvedThemeByAttributes?: ColorScheme,
 }
 
 export const useNextThemes = (): UseThemeProps & UseThemeExtendedProps => {
@@ -37,9 +32,9 @@ export const useNextThemes = (): UseThemeProps & UseThemeExtendedProps => {
 
   return Object.assign(props, {
     theme: props.theme as Themes,
-    resolvedTheme: props.resolvedTheme as ResolvedThemes,
-    useOsSettings: props.theme === Themes.system,
-    isDarkMode: props.resolvedTheme === ResolvedThemes.dark,
-    resolvedThemeByAttributes: isClient() ? document.documentElement.getAttribute(ATTRIBUTE) as ResolvedThemes : undefined,
+    resolvedTheme: props.resolvedTheme as ColorScheme,
+    useOsSettings: props.theme === Themes.SYSTEM,
+    isDarkMode: props.resolvedTheme === ColorScheme.DARK,
+    resolvedThemeByAttributes: isClient() ? document.documentElement.getAttribute(ATTRIBUTE) as ColorScheme : undefined,
   });
 };

+ 1 - 0
packages/core/src/index.ts

@@ -14,6 +14,7 @@ export * as pageUtils from './utils/page-utils';
 // export all
 export * from './plugin/interfaces/option-parser';
 export * from './interfaces/attachment';
+export * from './interfaces/color-scheme';
 export * from './interfaces/common';
 export * from './interfaces/growi-facade';
 export * from './interfaces/growi-theme-metadata';

+ 5 - 0
packages/core/src/interfaces/color-scheme.ts

@@ -0,0 +1,5 @@
+export const ColorScheme = {
+  LIGHT: 'light',
+  DARK: 'dark',
+} as const;
+export type ColorScheme = typeof ColorScheme[keyof typeof ColorScheme];

+ 3 - 2
packages/core/src/interfaces/growi-theme-metadata.ts

@@ -1,7 +1,8 @@
+import { ColorScheme } from './color-scheme';
+
 export const GrowiThemeSchemeType = {
+  ...ColorScheme,
   BOTH: 'both',
-  LIGHT: 'light',
-  DARK: 'dark',
 } as const;
 export type GrowiThemeSchemeType = typeof GrowiThemeSchemeType[keyof typeof GrowiThemeSchemeType];