Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wired up visibility settings for CTA card #1440

Merged
merged 1 commit into from
Feb 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ export function ColorPickerSetting({label, isExpanded, onSwatchChange, onPickerC
document.removeEventListener('click', closePicker);
};
}
}, [isExpanded]);
}, [isExpanded, onTogglePicker]);

return (
<div className="flex-col" data-testid={dataTestId} onClick={markClickedInside}>
Expand Down
4 changes: 2 additions & 2 deletions packages/koenig-lexical/src/components/ui/TabView.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const TabView = ({tabs, defaultTab, tabContent}) => {
</button>
))}
</div>
<div className="flex flex-col gap-3 p-6 pt-4">
<div className="flex flex-col gap-3 p-6 pt-4" data-testid={`tab-contents-${activeTab}`}>
{tabContent[activeTab]}
</div>
</>
Expand All @@ -47,4 +47,4 @@ TabView.propTypes = {
tabContent: PropTypes.objectOf(PropTypes.node).isRequired
};

export {TabView};
export {TabView};
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export function VisibilitySettings({visibilityOptions, toggleVisibility}) {
});

return (
<div key={group.key} className="flex flex-col gap-3">
<div key={group.key} className="flex flex-col gap-3" data-testid="visibility-settings">
<p className="text-sm font-bold tracking-normal text-grey-900 dark:text-grey-300">{group.label}</p>
{toggles}
{index < visibilityOptions.length - 1 && (
Expand Down
51 changes: 20 additions & 31 deletions packages/koenig-lexical/src/components/ui/cards/CtaCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import clsx from 'clsx';
import {Button} from '../Button';
import {ButtonGroupSetting, ColorOptionSetting, ColorPickerSetting, InputSetting, InputUrlSetting, MediaUploadSetting, SettingsPanel, ToggleSetting} from '../SettingsPanel';
import {ReadOnlyOverlay} from '../ReadOnlyOverlay';
import {VisibilitySettings} from '../VisibilitySettings';
import {getAccentColor} from '../../../utils/getAccentColor';
import {textColorForBackgroundColor} from '@tryghost/color-utils';

Expand Down Expand Up @@ -72,6 +73,7 @@ export function CtaCard({
isEditing = false,
layout = 'immersive',
showButton = false,
visibilityOptions = {},
handleButtonColor = () => {},
handleColorChange = () => {},
onFileChange = () => {},
Expand All @@ -81,7 +83,8 @@ export function CtaCard({
updateButtonUrl = () => {},
updateHasSponsorLabel = () => {},
updateLayout = () => {},
updateShowButton = () => {}
updateShowButton = () => {},
toggleVisibility = () => {}
}) {
const [buttonColorPickerExpanded, setButtonColorPickerExpanded] = useState(false);

Expand Down Expand Up @@ -193,26 +196,10 @@ export function CtaCard({
);

const visibilitySettings = (
<>
<p className="text-sm font-bold tracking-normal text-grey-900 dark:text-grey-300">Web</p>
<ToggleSetting
label="Anonymous visitors"
/>
<ToggleSetting
label="Free members"
/>
<ToggleSetting
label="Paid members"
/>
<hr className="not-kg-prose my-2 block border-t-grey-300 dark:border-t-grey-900" />
<p className="text-sm font-bold tracking-normal text-grey-900 dark:text-grey-300">Email</p>
<ToggleSetting
label="Free members"
/>
<ToggleSetting
label="Paid members"
/>
</>
<VisibilitySettings
toggleVisibility={toggleVisibility}
visibilityOptions={visibilityOptions}
/>
);

return (
Expand Down Expand Up @@ -247,13 +234,13 @@ export function CtaCard({
'block',
layout === 'immersive' ? 'w-full' : 'w-16 shrink-0'
)}>
<img
alt="Placeholder"
<img
alt="Placeholder"
className={clsx(
layout === 'immersive' ? 'h-auto w-full' : 'aspect-square w-16 object-cover',
'rounded-md'
)}
src={imageSrc}
)}
src={imageSrc}
/>
</div>
)}
Expand All @@ -279,10 +266,10 @@ export function CtaCard({
{/* Button */}
{ (showButton && (isEditing || (buttonText && buttonUrl))) &&
<div data-test-cta-button-current-url={buttonUrl}>
<Button
color={'accent'}
dataTestId="cta-button"
placeholder="Add button text"
<Button
color={'accent'}
dataTestId="cta-button"
placeholder="Add button text"
size={layout === 'immersive' ? 'medium' : 'small'}
style={buttonColor ? {
backgroundColor: buttonColor === 'accent' ? 'var(--accent-color)' : buttonColor,
Expand Down Expand Up @@ -322,7 +309,7 @@ CtaCard.propTypes = {
buttonColor: PropTypes.string,
buttonTextColor: PropTypes.string,
color: PropTypes.oneOf(['none', 'grey', 'white', 'blue', 'green', 'yellow', 'red']),
hasSponsorLabel: PropTypes.bool,
hasSponsorLabel: PropTypes.bool,
imageSrc: PropTypes.string,
isEditing: PropTypes.bool,
layout: PropTypes.oneOf(['minimal', 'immersive']),
Expand All @@ -338,5 +325,7 @@ CtaCard.propTypes = {
handleButtonColor: PropTypes.func,
onFileChange: PropTypes.func,
setFileInputRef: PropTypes.func,
onRemoveMedia: PropTypes.func
onRemoveMedia: PropTypes.func,
visibilityOptions: PropTypes.object,
toggleVisibility: PropTypes.func
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {CtaCard} from '../components/ui/cards/CtaCard';
import {SnippetActionToolbar} from '../components/ui/SnippetActionToolbar.jsx';
import {ToolbarMenu, ToolbarMenuItem, ToolbarMenuSeparator} from '../components/ui/ToolbarMenu.jsx';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {useVisibilityToggle} from '../hooks/useVisibilityToggle.js';

export const CallToActionNodeComponent = ({
nodeKey,
Expand All @@ -28,6 +29,9 @@ export const CallToActionNodeComponent = ({
const {isEditing, isSelected, setEditing} = React.useContext(CardContext);
const {fileUploader, cardConfig} = React.useContext(KoenigComposerContext);
const [showSnippetToolbar, setShowSnippetToolbar] = React.useState(false);

const {visibilityOptions, toggleVisibility} = useVisibilityToggle(editor, nodeKey, cardConfig);

const handleToolbarEdit = (event) => {
event.preventDefault();
event.stopPropagation();
Expand Down Expand Up @@ -136,11 +140,13 @@ export const CallToActionNodeComponent = ({
setFileInputRef={ref => fileInputRef.current = ref}
showButton={showButton}
text={textValue}
toggleVisibility={toggleVisibility}
updateButtonText={handleButtonTextChange}
updateButtonUrl={handleButtonUrlChange}
updateHasSponsorLabel={handleHasSponsorLabelChange}
updateLayout={handleUpdatingLayout}
updateShowButton={toggleShowButton}
visibilityOptions={visibilityOptions}
onFileChange={onFileChange}
onRemoveMedia={onRemoveMedia}
/>
Expand Down
17 changes: 10 additions & 7 deletions packages/koenig-lexical/src/nodes/HtmlNodeComponent.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,6 @@ export function HtmlNodeComponent({nodeKey, html}) {
{id: 'visibility', label: 'Visibility'}
];

const visibilitySettings = <VisibilitySettings toggleVisibility={toggleVisibility} visibilityOptions={visibilityOptions} />;

const settingsTabContents = {
visibility: visibilitySettings
};

const updateHtml = (value) => {
editor.update(() => {
const node = $getNodeByKey(nodeKey);
Expand All @@ -52,6 +46,13 @@ export function HtmlNodeComponent({nodeKey, html}) {
}
};

const visibilitySettings = (
<VisibilitySettings
toggleVisibility={toggleVisibility}
visibilityOptions={visibilityOptions}
/>
);

return (
<>
<HtmlCard
Expand Down Expand Up @@ -100,7 +101,9 @@ export function HtmlNodeComponent({nodeKey, html}) {
tabs={settingsTabs}
onMouseDown={e => e.preventDefault()}
>
{settingsTabContents}
{{
visibility: visibilitySettings
}}
</SettingsPanel>
)}
</>
Expand Down
108 changes: 93 additions & 15 deletions packages/koenig-lexical/test/e2e/cards/cta-card.test.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,35 @@
import path from 'path';
import {assertHTML, focusEditor, html, initialize, insertCard} from '../../utils/e2e';
import {assertHTML, focusEditor, getEditorStateJSON, html, initialize, insertCard} from '../../utils/e2e';
import {expect, test} from '@playwright/test';
import {fileURLToPath} from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

test.describe('Call To Action Card', async () => {
let page;
let serializedTestCard;

test.beforeAll(async ({browser}) => {
page = await browser.newPage();
});

test.beforeEach(async () => {
await initialize({page, uri: '/#/?content=false&labs=contentVisibility,contentVisibilityAlpha'});

serializedTestCard = {
type: 'call-to-action',
backgroundColor: 'green',
buttonColor: '#F0F0F0',
buttonText: 'Get access now',
buttonTextColor: '#000000',
buttonUrl: 'http://someblog.com/somepost',
hasImage: true,
hasSponsorLabel: true,
imageUrl: '/content/images/2022/11/koenig-lexical.jpg',
layout: 'minimal',
showButton: true,
textValue: '<p><span style="white-space: pre-wrap;">This is a new CTA Card.</span></p>'
};
});

test.afterAll(async () => {
Expand All @@ -23,20 +39,7 @@ test.describe('Call To Action Card', async () => {
test('can import serialized CTA card nodes', async function () {
const contentParam = encodeURIComponent(JSON.stringify({
root: {
children: [{
type: 'call-to-action',
backgroundColor: 'green',
buttonColor: '#F0F0F0',
buttonText: 'Get access now',
buttonTextColor: '#000000',
buttonUrl: 'http://someblog.com/somepost',
hasImage: true,
hasSponsorLabel: true,
imageUrl: '/content/images/2022/11/koenig-lexical.jpg',
layout: 'minimal',
showButton: true,
textValue: '<p><span style="white-space: pre-wrap;">This is a new CTA Card.</span></p>'
}],
children: [serializedTestCard],
direction: null,
format: '',
indent: 0,
Expand Down Expand Up @@ -362,4 +365,79 @@ test.describe('Call To Action Card', async () => {
const previewImage = await page.locator('[data-testid="media-upload-filled"] img');
await expect(previewImage).toBeVisible();
});

test('can change visibility settings', async function () {
await focusEditor(page);
const card = await insertCard(page, {cardName: 'call-to-action'});

// switch to visibility settings
await card.getByTestId('tab-visibility').click();

// check visibility icon and toggles match default state
await expect(page.getByTestId('visibility-indicator')).not.toBeVisible();
await expect(card.getByTestId('visibility-toggle-web-nonMembers')).toBeChecked();
await expect(card.getByTestId('visibility-toggle-web-freeMembers')).toBeChecked();
await expect(card.getByTestId('visibility-toggle-web-paidMembers')).toBeChecked();
await expect(card.getByTestId('visibility-toggle-email-freeMembers')).toBeChecked();
await expect(card.getByTestId('visibility-toggle-email-paidMembers')).toBeChecked();

// change toggles
await card.getByTestId('visibility-toggle-web-paidMembers').click();
await card.getByTestId('visibility-toggle-email-paidMembers').click();

// visibility icon appears
await expect(page.getByTestId('visibility-indicator')).toBeVisible();

// serialized state gets updated
const serializedState = JSON.parse(await getEditorStateJSON(page));
expect(serializedState.root.children[0].visibility).toEqual({
web: {
nonMember: true,
memberSegment: 'status:free'
},
email: {
memberSegment: 'status:free'
}
});
});

test('can import serialized visibility settings', async function () {
const contentParam = encodeURIComponent(JSON.stringify({
root: {
children: [{
...serializedTestCard,
visibility: {
web: {
nonMember: false,
memberSegment: 'status:free'
},
email: {
memberSegment: 'status:free'
}
}
}],
direction: null,
format: '',
indent: 0,
type: 'root',
version: 1
}
}));

await initialize({page, uri: `/#/?content=${contentParam}`});

// assert visibility icon is visible
await expect(page.getByTestId('visibility-indicator')).toBeVisible();

// put card into edit mode
await page.dblclick('[data-kg-card="call-to-action"]');

// check toggles match the serialized data
await page.click('[data-testid="tab-visibility"]');
await expect(page.getByTestId('visibility-toggle-web-nonMembers')).not.toBeChecked();
await expect(page.getByTestId('visibility-toggle-web-freeMembers')).toBeChecked();
await expect(page.getByTestId('visibility-toggle-web-paidMembers')).not.toBeChecked();
await expect(page.getByTestId('visibility-toggle-email-freeMembers')).toBeChecked();
await expect(page.getByTestId('visibility-toggle-email-paidMembers')).not.toBeChecked();
});
});
2 changes: 2 additions & 0 deletions packages/koenig-lexical/test/utils/e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@
ignoreDataTestId,
ignoreCardCaptionContents
});
expect(actual).toEqual(expected);

Check failure on line 105 in packages/koenig-lexical/test/utils/e2e.js

View workflow job for this annotation

GitHub Actions / Node 20.11.1

[chromium] › cards/gallery-card.test.js:158:5 › Gallery card › can upload images

1) [chromium] › cards/gallery-card.test.js:158:5 › Gallery card › can upload images ────────────── Error: expect(received).toEqual(expected) // deep equality - Expected - 0 + Received + 3 @@ -21,10 +21,13 @@ </div> </div> </div> </div> </div> + <div> + <div><div></div></div> + </div> <form> <input accept="image/gif,image/jpg,image/jpeg,image/png,image/svg+xml,image/webp" hidden="" multiple="" at ../utils/e2e.js:105 103 | ignoreCardCaptionContents 104 | }); > 105 | expect(actual).toEqual(expected); | ^ 106 | } 107 | 108 | export function prettifyHTML(string, options = {}) { at assertHTML (/home/runner/work/Koenig/Koenig/packages/koenig-lexical/test/utils/e2e.js:105:20) at /home/runner/work/Koenig/Koenig/packages/koenig-lexical/test/e2e/cards/gallery-card.test.js:171:9
}

export function prettifyHTML(string, options = {}) {
Expand Down Expand Up @@ -409,8 +409,10 @@
// hr is the one case we don't match the card name to the data attribute
if (card === 'Divider') {
await expect(page.locator(`[data-kg-card="horizontalrule"]`).nth(nth)).toBeVisible();
return page.locator(`[data-kg-card="horizontalrule"]`).nth(nth);
} else {
await expect(page.locator(`[data-kg-card="${cardName}" i]`).nth(nth)).toBeVisible();
return page.locator(`[data-kg-card="${cardName}" i]`).nth(nth);
}
}

Expand Down
Loading