0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-01-06 22:40:14 -05:00

🎨 Improved Access card layout in Settings (#21913)

As part of our efforts to add more personality to Settings, we've
identified areas where we can improve the UX.

This change makes it easier to manipulate Access settings by removing a
step from the flow.

fixes
https://linear.app/ghost/issue/DES-484/improve-access-card-layout-in-settings

**Before**
<img width="1718" alt="access-card-before"
src="https://github.com/user-attachments/assets/8c0f8f14-31b9-4712-93d2-97eb0f05c965"
/>

**After**
<img width="1718" alt="access-card-after"
src="https://github.com/user-attachments/assets/5831a7cc-fbad-4d4b-bccc-6e86be6a8c65"
/>
This commit is contained in:
Daniël van der Winden 2024-12-18 15:29:07 +01:00 committed by GitHub
parent 69474bb7db
commit 040b290fbd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 93 additions and 79 deletions

View file

@ -14,12 +14,12 @@ export interface SettingGroupContentProps {
} }
const SettingGroupContent: React.FC<SettingGroupContentProps> = ({columns, values, children, className}) => { const SettingGroupContent: React.FC<SettingGroupContentProps> = ({columns, values, children, className}) => {
let styles = 'flex flex-col gap-x-5 gap-y-7'; let styles = 'flex flex-col gap-x-5';
if (columns === 2) { if (columns === 2) {
styles = 'grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-6'; styles = 'grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-6';
} }
styles += ` ${className}`; styles += className ? ` ${className}` : ' gap-y-7';
return ( return (
<div className={styles}> <div className={styles}>

View file

@ -2,8 +2,8 @@ import React from 'react';
import TopLevelGroup from '../../TopLevelGroup'; import TopLevelGroup from '../../TopLevelGroup';
import useSettingGroup from '../../../hooks/useSettingGroup'; import useSettingGroup from '../../../hooks/useSettingGroup';
import {GroupBase, MultiValue} from 'react-select'; import {GroupBase, MultiValue} from 'react-select';
import {MultiSelect, MultiSelectOption, Select, SettingGroupContent, withErrorBoundary} from '@tryghost/admin-x-design-system'; import {MultiSelect, MultiSelectOption, Select, Separator, SettingGroupContent, withErrorBoundary} from '@tryghost/admin-x-design-system';
import {getOptionLabel} from '../../../utils/helpers'; // import {getOptionLabel} from '../../../utils/helpers';
import {getSettingValues} from '@tryghost/admin-x-framework/api/settings'; import {getSettingValues} from '@tryghost/admin-x-framework/api/settings';
import {useBrowseTiers} from '@tryghost/admin-x-framework/api/tiers'; import {useBrowseTiers} from '@tryghost/admin-x-framework/api/tiers';
@ -81,9 +81,9 @@ const Access: React.FC<{ keywords: string[] }> = ({keywords}) => {
'members_signup_access', 'default_content_visibility', 'default_content_visibility_tiers', 'comments_enabled' 'members_signup_access', 'default_content_visibility', 'default_content_visibility_tiers', 'comments_enabled'
]) as string[]; ]) as string[];
const membersSignupAccessLabel = getOptionLabel(MEMBERS_SIGNUP_ACCESS_OPTIONS, membersSignupAccess); // const membersSignupAccessLabel = getOptionLabel(MEMBERS_SIGNUP_ACCESS_OPTIONS, membersSignupAccess);
const defaultContentVisibilityLabel = getOptionLabel(DEFAULT_CONTENT_VISIBILITY_OPTIONS, defaultContentVisibility); // const defaultContentVisibilityLabel = getOptionLabel(DEFAULT_CONTENT_VISIBILITY_OPTIONS, defaultContentVisibility);
const commentsEnabledLabel = getOptionLabel(COMMENTS_ENABLED_OPTIONS, commentsEnabled); // const commentsEnabledLabel = getOptionLabel(COMMENTS_ENABLED_OPTIONS, commentsEnabled);
const {data: {tiers} = {}} = useBrowseTiers(); const {data: {tiers} = {}} = useBrowseTiers();
@ -106,71 +106,92 @@ const Access: React.FC<{ keywords: string[] }> = ({keywords}) => {
updateSetting('default_content_visibility_tiers', JSON.stringify(selectedTiers)); updateSetting('default_content_visibility_tiers', JSON.stringify(selectedTiers));
}; };
const values = ( // const values = (
<SettingGroupContent // <SettingGroupContent
values={[ // values={[
{ // {
heading: 'Subscription access', // heading: 'Subscription access',
key: 'subscription-access', // key: 'subscription-access',
value: membersSignupAccessLabel // value: membersSignupAccessLabel
}, // },
{ // {
heading: 'Default post access', // heading: 'Default post access',
key: 'default-post-access', // key: 'default-post-access',
value: defaultContentVisibilityLabel // value: defaultContentVisibilityLabel
}, // },
{ // {
heading: 'Commenting', // heading: 'Commenting',
key: 'commenting', // key: 'commenting',
value: commentsEnabledLabel // value: commentsEnabledLabel
} // }
]} // ]}
/> // />
); // );
const form = ( const form = (
<SettingGroupContent columns={1}> <SettingGroupContent className='gap-y-4' columns={1}>
<Select <div className="flex flex-col content-center items-center gap-4 md:flex-row">
hint='Who should be able to subscribe to your site?' <div className="w-full min-w-[160px] max-w-none md:w-2/3 md:max-w-[320px]">Who should be able to subscribe to your site?</div>
options={MEMBERS_SIGNUP_ACCESS_OPTIONS} <div className="w-full md:flex-1">
selectedOption={MEMBERS_SIGNUP_ACCESS_OPTIONS.find(option => option.value === membersSignupAccess)} <Select
testId='subscription-access-select' options={MEMBERS_SIGNUP_ACCESS_OPTIONS}
title="Subscription access" selectedOption={MEMBERS_SIGNUP_ACCESS_OPTIONS.find(option => option.value === membersSignupAccess)}
onSelect={(option) => { testId='subscription-access-select'
updateSetting('members_signup_access', option?.value || null); onSelect={(option) => {
}} updateSetting('members_signup_access', option?.value || null);
/> handleEditingChange(true);
<Select }}
hint='When a new post is created, who should have access?' />
options={DEFAULT_CONTENT_VISIBILITY_OPTIONS} </div>
selectedOption={DEFAULT_CONTENT_VISIBILITY_OPTIONS.find(option => option.value === defaultContentVisibility)} </div>
testId='default-post-access-select' <Separator />
title="Default post access" <div className="flex flex-col content-center items-center gap-4 md:flex-row">
onSelect={(option) => { <div className="w-full min-w-[160px] max-w-none md:w-2/3 md:max-w-[320px]">Who should have access to new posts?</div>
updateSetting('default_content_visibility', option?.value || null); <div className="w-full md:flex-1">
}} <Select
/> options={DEFAULT_CONTENT_VISIBILITY_OPTIONS}
selectedOption={DEFAULT_CONTENT_VISIBILITY_OPTIONS.find(option => option.value === defaultContentVisibility)}
testId='default-post-access-select'
onSelect={(option) => {
updateSetting('default_content_visibility', option?.value || null);
handleEditingChange(true);
}}
/>
</div>
</div>
{defaultContentVisibility === 'tiers' && ( {defaultContentVisibility === 'tiers' && (
<MultiSelect <div className="flex flex-col content-center items-center gap-4 md:flex-row">
color='black' <div className="w-full min-w-[160px] max-w-none md:w-2/3 md:max-w-[320px]">Select specific tiers</div>
options={tierOptionGroups.filter(group => group.options.length > 0)} <div className="w-full md:flex-1">
testId='tiers-select' <MultiSelect
title='Select tiers' color='black'
values={selectedTierOptions} options={tierOptionGroups.filter(group => group.options.length > 0)}
clearBg testId='tiers-select'
onChange={setSelectedTiers} values={selectedTierOptions}
/> onChange={(selectedOptions) => {
setSelectedTiers(selectedOptions);
handleEditingChange(true);
}}
/>
</div>
</div>
)} )}
<Select <Separator />
hint='Who can comment on posts?' <div className="flex flex-col content-center items-center gap-4 md:flex-row">
options={COMMENTS_ENABLED_OPTIONS} <div className="w-full min-w-[160px] max-w-none md:w-2/3 md:max-w-[320px]">Who can comment on posts?</div>
selectedOption={COMMENTS_ENABLED_OPTIONS.find(option => option.value === commentsEnabled)} <div className="w-full md:flex-1">
testId='commenting-select' <Select
title="Commenting" options={COMMENTS_ENABLED_OPTIONS}
onSelect={(option) => { selectedOption={COMMENTS_ENABLED_OPTIONS.find(option => option.value === commentsEnabled)}
updateSetting('comments_enabled', option?.value || null); testId='commenting-select'
}} title=""
/> onSelect={(option) => {
updateSetting('comments_enabled', option?.value || null);
handleEditingChange(true);
}}
/>
</div>
</div>
</SettingGroupContent> </SettingGroupContent>
); );
@ -183,11 +204,12 @@ const Access: React.FC<{ keywords: string[] }> = ({keywords}) => {
saveState={saveState} saveState={saveState}
testId='access' testId='access'
title='Access' title='Access'
hideEditButton
onCancel={handleCancel} onCancel={handleCancel}
onEditingChange={handleEditingChange} onEditingChange={handleEditingChange}
onSave={handleSave} onSave={handleSave}
> >
{isEditing ? form : values} {form}
</TopLevelGroup> </TopLevelGroup>
); );
}; };

View file

@ -21,16 +21,12 @@ test.describe('Access settings', async () => {
await expect(section.getByText('Public')).toHaveCount(1); await expect(section.getByText('Public')).toHaveCount(1);
await expect(section.getByText('Nobody')).toHaveCount(1); await expect(section.getByText('Nobody')).toHaveCount(1);
await section.getByRole('button', {name: 'Edit'}).click();
await chooseOptionInSelect(section.getByTestId('subscription-access-select'), 'Only people I invite'); await chooseOptionInSelect(section.getByTestId('subscription-access-select'), 'Only people I invite');
await chooseOptionInSelect(section.getByTestId('default-post-access-select'), /^Members only$/); await chooseOptionInSelect(section.getByTestId('default-post-access-select'), /^Members only$/);
await chooseOptionInSelect(section.getByTestId('commenting-select'), 'All members'); await chooseOptionInSelect(section.getByTestId('commenting-select'), 'All members');
await section.getByRole('button', {name: 'Save'}).click(); await section.getByRole('button', {name: 'Save'}).click();
await expect(section.getByTestId('subscription-access-select')).toHaveCount(0);
await expect(section.getByText('Only people I invite')).toHaveCount(1); await expect(section.getByText('Only people I invite')).toHaveCount(1);
await expect(section.getByText('Members only')).toHaveCount(1); await expect(section.getByText('Members only')).toHaveCount(1);
await expect(section.getByText('All members')).toHaveCount(1); await expect(section.getByText('All members')).toHaveCount(1);
@ -56,8 +52,6 @@ test.describe('Access settings', async () => {
const section = page.getByTestId('access'); const section = page.getByTestId('access');
await section.getByRole('button', {name: 'Edit'}).click();
await chooseOptionInSelect(section.getByTestId('subscription-access-select'), 'Nobody'); await chooseOptionInSelect(section.getByTestId('subscription-access-select'), 'Nobody');
await section.getByRole('button', {name: 'Save'}).click(); await section.getByRole('button', {name: 'Save'}).click();
@ -68,7 +62,7 @@ test.describe('Access settings', async () => {
] ]
}); });
await expect(section.getByTestId('subscription-access-select')).toHaveCount(0); await expect(section.getByTestId('subscription-access-select')).toContainText('Nobody');
await expect(page.getByTestId('portal').getByRole('button', {name: 'Customize'})).toBeDisabled(); await expect(page.getByTestId('portal').getByRole('button', {name: 'Customize'})).toBeDisabled();
await expect(page.getByTestId('enable-newsletters')).toContainText('only existing members will receive newsletters'); await expect(page.getByTestId('enable-newsletters')).toContainText('only existing members will receive newsletters');
@ -88,8 +82,6 @@ test.describe('Access settings', async () => {
const section = page.getByTestId('access'); const section = page.getByTestId('access');
await section.getByRole('button', {name: 'Edit'}).click();
await chooseOptionInSelect(section.getByTestId('default-post-access-select'), 'Specific tiers'); await chooseOptionInSelect(section.getByTestId('default-post-access-select'), 'Specific tiers');
await section.getByTestId('tiers-select').click(); await section.getByTestId('tiers-select').click();
@ -98,7 +90,7 @@ test.describe('Access settings', async () => {
await section.getByRole('button', {name: 'Save'}).click(); await section.getByRole('button', {name: 'Save'}).click();
await expect(section.getByText('Specific tiers')).toHaveCount(1); await expect(section.getByTestId('default-post-access-select')).toContainText('Specific tiers');
expect(lastApiRequests.editSettings?.body).toEqual({ expect(lastApiRequests.editSettings?.body).toEqual({
settings: [ settings: [