From 84d202633071a372c8e5f7ba28ae579ed9945759 Mon Sep 17 00:00:00 2001 From: Riley Pearce <54197972+rilrom@users.noreply.github.com> Date: Thu, 26 Sep 2024 23:39:29 +0930 Subject: [PATCH] feat: preselected theme (#8354) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR implements the ability to attempt to force the use of light/dark theme in the admin panel. While I am a big advocate for the benefits that dark mode can bring to UX, it does not always suit a clients branding needs. Open to discussion on whether we consider this a suitable feature for the platform. Please feel free to add to this PR as needed. TODO: - [x] Implement tests (I'm open to guidance on this from the Payload team as currently it doesn't look like it's possible to adjust the payload config file on the fly - meaning it can't be easily placed in the admin folder tests). --------- Co-authored-by: Germán Jabloñski <43938777+GermanJablo@users.noreply.github.com> --- docs/admin/overview.mdx | 1 + packages/next/src/utilities/getRequestTheme.ts | 4 ++++ .../next/src/views/Account/Settings/index.tsx | 7 ++++--- packages/next/src/views/Account/index.tsx | 8 ++++++-- packages/payload/src/config/defaults.ts | 1 + packages/payload/src/config/types.ts | 6 ++++++ packages/ui/src/providers/Root/index.tsx | 2 +- packages/ui/src/providers/Theme/index.tsx | 16 ++++++++++++---- test/admin-root/config.ts | 1 + test/admin-root/e2e.spec.ts | 7 +++++++ 10 files changed, 43 insertions(+), 10 deletions(-) diff --git a/docs/admin/overview.mdx b/docs/admin/overview.mdx index 0ec100711c..7617e6b0e1 100644 --- a/docs/admin/overview.mdx +++ b/docs/admin/overview.mdx @@ -98,6 +98,7 @@ The following options are available: | **`livePreview`** | Enable real-time editing for instant visual feedback of your front-end application. [More details](../live-preview/overview). | | **`meta`** | Base metadata to use for the Admin Panel. [More details](./metadata). | | **`routes`** | Replace built-in Admin Panel routes with your own custom routes. [More details](#customizing-routes). | +| **`theme`** | Restrict the Admin Panel theme to use only one of your choice. Default is `all`. | **`user`** | The `slug` of the Collection that you want to allow to login to the Admin Panel. [More details](#the-admin-user-collection). | diff --git a/packages/next/src/utilities/getRequestTheme.ts b/packages/next/src/utilities/getRequestTheme.ts index 8b447038c9..0ed2328080 100644 --- a/packages/next/src/utilities/getRequestTheme.ts +++ b/packages/next/src/utilities/getRequestTheme.ts @@ -12,6 +12,10 @@ type GetRequestLanguageArgs = { const acceptedThemes: Theme[] = ['dark', 'light'] export const getRequestTheme = ({ config, cookies, headers }: GetRequestLanguageArgs): Theme => { + if (config.admin.theme !== 'all' && acceptedThemes.includes(config.admin.theme)) { + return config.admin.theme + } + const themeCookie = cookies.get(`${config.cookiePrefix || 'payload'}-theme`) const themeFromCookie: Theme = ( diff --git a/packages/next/src/views/Account/Settings/index.tsx b/packages/next/src/views/Account/Settings/index.tsx index 7b0d5251e9..38380d2dc7 100644 --- a/packages/next/src/views/Account/Settings/index.tsx +++ b/packages/next/src/views/Account/Settings/index.tsx @@ -1,5 +1,5 @@ import type { I18n } from '@payloadcms/translations' -import type { LanguageOptions } from 'payload' +import type { Config, LanguageOptions } from 'payload' import { FieldLabel } from '@payloadcms/ui' import React from 'react' @@ -14,8 +14,9 @@ export const Settings: React.FC<{ readonly className?: string readonly i18n: I18n readonly languageOptions: LanguageOptions + readonly theme: Config['admin']['theme'] }> = (props) => { - const { className, i18n, languageOptions } = props + const { className, i18n, languageOptions, theme } = props return (
@@ -24,7 +25,7 @@ export const Settings: React.FC<{
- + {theme === 'all' && } ) } diff --git a/packages/next/src/views/Account/index.tsx b/packages/next/src/views/Account/index.tsx index cc19f5f050..e586bfe0f8 100644 --- a/packages/next/src/views/Account/index.tsx +++ b/packages/next/src/views/Account/index.tsx @@ -38,7 +38,11 @@ export const Account: React.FC = async ({ } = initPageResult const { - admin: { components: { views: { Account: CustomAccountComponent } = {} } = {}, user: userSlug }, + admin: { + components: { views: { Account: CustomAccountComponent } = {} } = {}, + theme, + user: userSlug, + }, routes: { api }, serverURL, } = config @@ -85,7 +89,7 @@ export const Account: React.FC = async ({ return ( } + AfterFields={} apiURL={`${serverURL}${api}/${userSlug}${user?.id ? `/${user.id}` : ''}`} collectionSlug={userSlug} docPermissions={docPermissions} diff --git a/packages/payload/src/config/defaults.ts b/packages/payload/src/config/defaults.ts index 27976c19e0..37a1a64c20 100644 --- a/packages/payload/src/config/defaults.ts +++ b/packages/payload/src/config/defaults.ts @@ -25,6 +25,7 @@ export const defaults: Omit = { reset: '/reset', unauthorized: '/unauthorized', }, + theme: 'all', }, bin: [], collections: [], diff --git a/packages/payload/src/config/types.ts b/packages/payload/src/config/types.ts index 4b05ca2db8..a1e5af09ca 100644 --- a/packages/payload/src/config/types.ts +++ b/packages/payload/src/config/types.ts @@ -818,6 +818,12 @@ export type Config = { /** The route for the unauthorized page. */ unauthorized?: string } + /** + * Restrict the Admin Panel theme to use only one of your choice + * + * @default 'all' // The theme can be configured by users + */ + theme?: 'all' | 'dark' | 'light' /** The slug of a Collection that you want to be used to log in to the Admin dashboard. */ user?: string } diff --git a/packages/ui/src/providers/Root/index.tsx b/packages/ui/src/providers/Root/index.tsx index 48e5264bc3..e005d368fc 100644 --- a/packages/ui/src/providers/Root/index.tsx +++ b/packages/ui/src/providers/Root/index.tsx @@ -88,7 +88,7 @@ export const RootProvider: React.FC = ({ - + diff --git a/packages/ui/src/providers/Theme/index.tsx b/packages/ui/src/providers/Theme/index.tsx index ac4ebaeb5a..43f92a2bd0 100644 --- a/packages/ui/src/providers/Theme/index.tsx +++ b/packages/ui/src/providers/Theme/index.tsx @@ -1,6 +1,8 @@ 'use client' import React, { createContext, useCallback, useContext, useEffect, useState } from 'react' +import { useConfig } from '../Config/index.js' + export type Theme = 'dark' | 'light' export type ThemeContext = { @@ -55,20 +57,26 @@ export const defaultTheme = 'light' export const ThemeProvider: React.FC<{ children?: React.ReactNode - cookiePrefix?: string theme?: Theme -}> = ({ children, cookiePrefix, theme: initialTheme }) => { - const cookieKey = `${cookiePrefix || 'payload'}-theme` +}> = ({ children, theme: initialTheme }) => { + const { config } = useConfig() + + const preselectedTheme = config.admin.theme + const cookieKey = `${config.cookiePrefix || 'payload'}-theme` const [theme, setThemeState] = useState(initialTheme || defaultTheme) const [autoMode, setAutoMode] = useState() useEffect(() => { + if (preselectedTheme !== 'all') { + return + } + const { theme, themeFromCookies } = getTheme(cookieKey) setThemeState(theme) setAutoMode(!themeFromCookies) - }, [cookieKey]) + }, [preselectedTheme, cookieKey]) const setTheme = useCallback( (themeToSet: 'auto' | Theme) => { diff --git a/test/admin-root/config.ts b/test/admin-root/config.ts index 7451fe6707..55af949856 100644 --- a/test/admin-root/config.ts +++ b/test/admin-root/config.ts @@ -21,6 +21,7 @@ export default buildConfigWithDefaults({ importMap: { baseDir: path.resolve(dirname), }, + theme: 'dark', }, cors: ['http://localhost:3000', 'http://localhost:3001'], globals: [MenuGlobal], diff --git a/test/admin-root/e2e.spec.ts b/test/admin-root/e2e.spec.ts index b58e127c9f..fdfdb7cec6 100644 --- a/test/admin-root/e2e.spec.ts +++ b/test/admin-root/e2e.spec.ts @@ -98,4 +98,11 @@ test.describe('Admin Panel (Root)', () => { await expect(favicons.nth(1)).toHaveAttribute('media', '(prefers-color-scheme: dark)') await expect(favicons.nth(1)).toHaveAttribute('href', /\/payload-favicon-light\.[a-z\d]+\.png/) }) + + test('config.admin.theme should restrict the theme', async () => { + await page.goto(url.account) + await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark') + await expect(page.locator('#field-theme')).toBeHidden() + await expect(page.locator('#field-theme-auto')).toBeHidden() + }) })