Port custom theme setting visibility to admin-x (#18367)

refs https://github.com/TryGhost/Product/issues/3962

Port custom theme setting visibility to admin-x based on previous
implementation in the Ghost admin:
https://github.com/TryGhost/Ghost/pull/17920
This commit is contained in:
Michael Barrett 2023-10-02 15:54:14 +01:00 committed by GitHub
parent b5fc527f8d
commit 043c9bb35d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 135 additions and 8 deletions

View File

@ -0,0 +1,4 @@
/**
* This is used by vite to resolve node builtins. See resolve.alias in vite.config.js
*/
module.exports = {};

View File

@ -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",

View File

@ -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<CustomThemeSettingsResp
update: newData => newData
}
});
export function isCustomThemeSettingVisible(setting: CustomThemeSetting, settingsKeyValueObj: Record<string, string>) {
if (!setting.visibility) {
return true;
}
return nql(setting.visibility).queryJSON(settingsKeyValueObj);
}

View File

@ -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();
}

View File

@ -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 (
<SettingGroupContent className='mt-7'>
{settings.map(setting => <ThemeSetting key={setting.key} setSetting={value => updateSetting({...setting, value} as CustomThemeSetting)} setting={setting} />)}
{filteredSettings.map(setting => <ThemeSetting key={setting.key} setSetting={value => updateSetting({...setting, value} as CustomThemeSetting)} setting={setting} />)}
</SettingGroupContent>
);
};

View File

@ -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');

View File

@ -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: '<html><head><style></style></head><body><div>homepage preview</div></body></html>'
});
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'}
]
});
});
});

View File

@ -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);
});
});

View File

@ -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')
}
}
});
});