diff --git a/apps/admin-x-settings/node-shim.cjs b/apps/admin-x-settings/node-shim.cjs new file mode 100644 index 0000000000..3f40d5a474 --- /dev/null +++ b/apps/admin-x-settings/node-shim.cjs @@ -0,0 +1,4 @@ +/** + * This is used by vite to resolve node builtins. See resolve.alias in vite.config.js + */ +module.exports = {}; diff --git a/apps/admin-x-settings/package.json b/apps/admin-x-settings/package.json index 0a08904f9e..b1728863dd 100644 --- a/apps/admin-x-settings/package.json +++ b/apps/admin-x-settings/package.json @@ -47,6 +47,7 @@ "@tanstack/react-query": "4.35.7", "@tryghost/color-utils": "0.1.24", "@tryghost/limit-service": "^1.2.10", + "@tryghost/nql": "0.11.0", "@tryghost/timezone-data": "0.3.0", "@uiw/react-codemirror": "^4.21.9", "clsx": "2.0.0", diff --git a/apps/admin-x-settings/src/api/customThemeSettings.ts b/apps/admin-x-settings/src/api/customThemeSettings.ts index 4c436245b5..5503995ec9 100644 --- a/apps/admin-x-settings/src/api/customThemeSettings.ts +++ b/apps/admin-x-settings/src/api/customThemeSettings.ts @@ -1,3 +1,4 @@ +import nql from '@tryghost/nql'; import {Setting} from './settings'; import {createMutation, createQuery} from '../utils/api/hooks'; @@ -19,8 +20,11 @@ export type CustomThemeSetting = CustomThemeSettingData & { description?: string // homepage and post are the only two groups we handle, but technically theme authors can put other things in package.json group?: 'homepage' | 'post' | string + visibility?: string } +export const hiddenCustomThemeSettingValue = null; + export interface CustomThemeSettingsResponseType { custom_theme_settings: CustomThemeSetting[]; } @@ -45,3 +49,11 @@ export const useEditCustomThemeSettings = createMutation newData } }); + +export function isCustomThemeSettingVisible(setting: CustomThemeSetting, settingsKeyValueObj: Record) { + if (!setting.visibility) { + return true; + } + + return nql(setting.visibility).queryJSON(settingsKeyValueObj); +} diff --git a/apps/admin-x-settings/src/components/settings/site/designAndBranding/ThemePreview.tsx b/apps/admin-x-settings/src/components/settings/site/designAndBranding/ThemePreview.tsx index f550825e35..4f85f00a83 100644 --- a/apps/admin-x-settings/src/components/settings/site/designAndBranding/ThemePreview.tsx +++ b/apps/admin-x-settings/src/components/settings/site/designAndBranding/ThemePreview.tsx @@ -1,6 +1,6 @@ import IframeBuffering from '../../../../utils/IframeBuffering'; import React, {useCallback} from 'react'; -import {CustomThemeSetting} from '../../../../api/customThemeSettings'; +import {CustomThemeSetting, hiddenCustomThemeSettingValue, isCustomThemeSettingVisible} from '../../../../api/customThemeSettings'; type BrandSettings = { description: string; @@ -35,6 +35,7 @@ function getPreviewData({ if (!themeSettings) { return; } + const themeSettingsKeyValueObj = themeSettings.reduce((obj, {key, value}) => ({...obj, [key]: value}), {}); const params = new URLSearchParams(); params.append('c', accentColor); @@ -42,13 +43,13 @@ function getPreviewData({ params.append('icon', icon); params.append('logo', logo); params.append('cover', coverImage); - const themeSettingsObj: { - [key: string]: string; + const custom: { + [key: string]: string | typeof hiddenCustomThemeSettingValue; } = {}; themeSettings.forEach((setting) => { - themeSettingsObj[setting.key] = setting.value as string; + custom[setting.key] = isCustomThemeSettingVisible(setting, themeSettingsKeyValueObj) ? setting.value as string : hiddenCustomThemeSettingValue; }); - params.append('custom', JSON.stringify(themeSettingsObj)); + params.append('custom', JSON.stringify(custom)); return params.toString(); } diff --git a/apps/admin-x-settings/src/components/settings/site/designAndBranding/ThemeSettings.tsx b/apps/admin-x-settings/src/components/settings/site/designAndBranding/ThemeSettings.tsx index 91eb1c67da..7167e814ef 100644 --- a/apps/admin-x-settings/src/components/settings/site/designAndBranding/ThemeSettings.tsx +++ b/apps/admin-x-settings/src/components/settings/site/designAndBranding/ThemeSettings.tsx @@ -8,7 +8,7 @@ import SettingGroupContent from '../../../../admin-x-ds/settings/SettingGroupCon import TextField from '../../../../admin-x-ds/global/form/TextField'; import Toggle from '../../../../admin-x-ds/global/form/Toggle'; import useHandleError from '../../../../utils/api/handleError'; -import {CustomThemeSetting} from '../../../../api/customThemeSettings'; +import {CustomThemeSetting, isCustomThemeSettingVisible} from '../../../../api/customThemeSettings'; import {getImageUrl, useUploadImage} from '../../../../api/images'; import {humanizeSettingKey} from '../../../../api/settings'; @@ -85,9 +85,13 @@ const ThemeSetting: React.FC<{ }; const ThemeSettings: React.FC<{ settings: CustomThemeSetting[], updateSetting: (setting: CustomThemeSetting) => void }> = ({settings, updateSetting}) => { + // Filter out custom theme settings that should not be visible + const settingsKeyValueObj = settings.reduce((obj, {key, value}) => ({...obj, [key]: value}), {}); + const filteredSettings = settings.filter(setting => isCustomThemeSettingVisible(setting, settingsKeyValueObj)); + return ( - {settings.map(setting => updateSetting({...setting, value} as CustomThemeSetting)} setting={setting} />)} + {filteredSettings.map(setting => updateSetting({...setting, value} as CustomThemeSetting)} setting={setting} />)} ); }; diff --git a/apps/admin-x-settings/src/typings.d.ts b/apps/admin-x-settings/src/typings.d.ts index bf6dfe4761..d8c213c872 100644 --- a/apps/admin-x-settings/src/typings.d.ts +++ b/apps/admin-x-settings/src/typings.d.ts @@ -1,6 +1,7 @@ declare module '@tryghost/timezone-data' declare module '@tryghost/limit-service' declare module '@tryghost/color-utils' +declare module '@tryghost/nql' declare module '*.svg' { import React = require('react'); diff --git a/apps/admin-x-settings/test/e2e/site/design.test.ts b/apps/admin-x-settings/test/e2e/site/design.test.ts index 6ee62c012d..673d5f22a8 100644 --- a/apps/admin-x-settings/test/e2e/site/design.test.ts +++ b/apps/admin-x-settings/test/e2e/site/design.test.ts @@ -142,4 +142,69 @@ test.describe('Design settings', async () => { ] }); }); + + test('Custom theme setting visibility', async ({page}) => { + const {lastApiRequests} = await mockApi({page, requests: { + ...globalDataRequests, + editCustomThemeSettings: {method: 'PUT', path: '/custom_theme_settings/', response: responseFixtures.customThemeSettings}, + browseCustomThemeSettings: {method: 'GET', path: '/custom_theme_settings/', response: { + custom_theme_settings: [{ + type: 'select', + options: [ + 'Logo on cover', + 'Logo in the middle', + 'Stacked' + ], + default: 'Logo on cover', + id: '648047658d265b0c8b33c591', + value: 'Stacked', + key: 'navigation_layout' + }, { + type: 'boolean', + default: 'false', + id: '648047658d265b0c8b33c592', + value: 'false', + key: 'show_featured_posts', + visibility: 'navigation_layout:[Stacked]' + }] + }}, + browseLatestPost: {method: 'GET', path: /^\/posts\/.+limit=1/, response: responseFixtures.latestPost} + }}); + const lastPreviewRequest = await mockSitePreview({ + page, + url: responseFixtures.site.site.url, + response: '
homepage preview
' + }); + + await page.goto('/'); + + const section = page.getByTestId('design'); + + await section.getByRole('button', {name: 'Customize'}).click(); + + const modal = page.getByTestId('design-modal'); + + await modal.getByRole('tab', {name: 'Site wide'}).click(); + + const showFeaturedPostsCustomThemeSetting = modal.getByLabel('Show featured posts'); + + await expect(showFeaturedPostsCustomThemeSetting).toBeVisible(); + + await chooseOptionInSelect(modal.getByLabel('Navigation layout'), 'Logo in the middle'); + + await expect(showFeaturedPostsCustomThemeSetting).not.toBeVisible(); + + await modal.getByRole('button', {name: 'Save'}).click(); + + const expectedSettings = {navigation_layout: 'Logo in the middle', show_featured_posts: null}; + const expectedEncoded = new URLSearchParams([['custom', JSON.stringify(expectedSettings)]]).toString(); + expect(lastPreviewRequest.previewHeader).toMatch(new RegExp(`&${expectedEncoded.replace(/\+/g, '\\+')}`)); + + expect(lastApiRequests.editCustomThemeSettings?.body).toMatchObject({ + custom_theme_settings: [ + {key: 'navigation_layout', value: 'Logo in the middle'}, + {key: 'show_featured_posts', value: 'false'} + ] + }); + }); }); diff --git a/apps/admin-x-settings/test/unit/api/customThemeSettings.ts b/apps/admin-x-settings/test/unit/api/customThemeSettings.ts new file mode 100644 index 0000000000..29c417e984 --- /dev/null +++ b/apps/admin-x-settings/test/unit/api/customThemeSettings.ts @@ -0,0 +1,28 @@ +import * as assert from 'assert/strict'; +import {CustomThemeSetting, isCustomThemeSettingVisible} from '../../../src/api/customThemeSettings'; + +describe('isCustomThemeSettingVisible', function () { + it('returns whether or not a custom theme setting is visible', function () { + const settings: CustomThemeSetting[] = [ + { + id: 'abc123', + key: 'foo', + type: 'boolean', + value: false, + default: true + }, + { + id: 'def456', + key: 'bar', + type: 'text', + value: 'qux', + default: 'qux', + visibility: 'foo:true' + } + ]; + const settingsKeyValueObj = settings.reduce((obj, {key, value}) => ({...obj, [key]: value}), {}); + + assert.equal(isCustomThemeSettingVisible(settings[0], settingsKeyValueObj), true); + assert.equal(isCustomThemeSettingVisible(settings[1], settingsKeyValueObj), false); + }); +}); diff --git a/apps/admin-x-settings/vite.config.ts b/apps/admin-x-settings/vite.config.ts index 55a6e86227..f8c81afd14 100644 --- a/apps/admin-x-settings/vite.config.ts +++ b/apps/admin-x-settings/vite.config.ts @@ -47,7 +47,8 @@ export default (function viteConfig() { ], define: { 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), - 'process.env.VITEST_SEGFAULT_RETRY': 3 + 'process.env.VITEST_SEGFAULT_RETRY': 3, + 'process.env.DEBUG': false // Shim env var utilized by the @tryghost/nql package }, preview: { port: 4174 @@ -80,6 +81,16 @@ export default (function viteConfig() { minThreads: 1, maxThreads: 2 }) + }, + resolve: { + // Shim node modules utilized by the @tryghost/nql package + alias: { + fs: 'node-shim.cjs', + path: 'node-shim.cjs', + util: 'node-shim.cjs', + // @TODO: Remove this when @tryghost/nql is updated + mingo: resolve(__dirname, '../../node_modules/mingo/dist/mingo.js') + } } }); });