Skip to content
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
@@ -0,0 +1,169 @@
import { siteHasFeature } from '@automattic/jetpack-script-data';
import { render } from '@testing-library/react';
import { useDispatch, useSelect } from '@wordpress/data';
import { PerNetworkCustomizationForm } from './per-network';
import type { Connection } from '../../../social-store/types';

jest.mock( '@automattic/jetpack-script-data', () => {
const actual = jest.requireActual( '@automattic/jetpack-script-data' );
return {
...actual,
siteHasFeature: jest.fn(),
};
} );

jest.mock( '@wordpress/data', () => {
const actual = jest.requireActual( '@wordpress/data' );
const mocks = {
useDispatch: jest.fn(),
useSelect: jest.fn(),
};

return new Proxy( actual, {
get( target, property ) {
return mocks[ property as keyof typeof mocks ] ?? target[ property as keyof typeof target ];
},
} );
} );

const mockUsePostMeta = jest.fn();
jest.mock( '../../../hooks/use-post-meta', () => ( {
usePostMeta: () => mockUsePostMeta(),
} ) );

jest.mock( '../../../hooks/use-featured-image', () => jest.fn( () => null ) );
jest.mock( '../../../hooks/use-media-details', () => jest.fn( () => [ null ] ) );

const mockSharePostForm = jest.fn();
jest.mock( '../../form/share-post-form', () => ( {
SharePostForm: ( props: unknown ) => {
mockSharePostForm( props );
return null;
},
} ) );

const mockUseDispatch = useDispatch as jest.Mock;
const mockUseSelect = useSelect as jest.Mock;
const mockSiteHasFeature = siteHasFeature as jest.Mock;

const baseConnection: Connection = {
connection_id: 'connection-1',
display_name: 'Example Connection',
external_handle: '@example',
external_id: 'external-1',
profile_link: 'https://example.com/profile',
profile_picture: 'https://example.com/profile.jpg',
service_label: 'Facebook',
service_name: 'facebook',
shared: false,
status: 'ok',
wpcom_user_id: 123,
enabled: true,
attached_media: [ { id: 99, url: 'https://example.com/media.jpg', type: 'image/jpeg' } ],
};

/**
* Render the form and return the props passed to SharePostForm.
*
* @param {Partial< Connection >} connectionOverrides - Partial connection values for the test case.
* @return {Record< string, unknown >} Props passed to SharePostForm.
*/
function getSharePostFormProps( connectionOverrides: Partial< Connection > = {} ) {
const connection = { ...baseConnection, ...connectionOverrides };
render( <PerNetworkCustomizationForm connection={ connection } /> );
return mockSharePostForm.mock.calls[ 0 ][ 0 ];
}

describe( 'PerNetworkCustomizationForm', () => {
const mockCustomizeConnectionById = jest.fn();
let globalMessage = '';
let globalTemplate = 'Saved global template';
let templatesEnabled = true;

beforeEach( () => {
jest.clearAllMocks();

globalMessage = '';
globalTemplate = 'Saved global template';
templatesEnabled = true;

mockUseDispatch.mockReturnValue( {
customizeConnectionById: mockCustomizeConnectionById,
} );

mockUsePostMeta.mockImplementation( () => ( {
attachedMedia: [],
shareMessage: globalMessage,
mediaSource: undefined,
} ) );

mockUseSelect.mockImplementation( selector => {
return selector( () => ( {
getSocialSettings: () => ( { messageTemplate: globalTemplate } ),
} ) );
} );

mockSiteHasFeature.mockImplementation( flag => {
return flag === 'social-message-templates' ? templatesEnabled : false;
} );
} );

it( 'uses the per-connection message when it is non-empty', () => {
const sharePostFormProps = getSharePostFormProps( {
message: 'Manual per-post override',
template: '{title} {url}',
} );

expect( sharePostFormProps.message ).toBe( 'Manual per-post override' );
expect( sharePostFormProps.messageHelp ).toBe( 'Connection template will be used if empty.' );
} );

it( 'prefills with the connection template when message is empty', () => {
const sharePostFormProps = getSharePostFormProps( {
message: '',
template: '{title} {url}',
} );

expect( sharePostFormProps.message ).toBe( '{title} {url}' );
expect( sharePostFormProps.messageHelp ).toBe( 'Connection template will be used if empty.' );
} );

it( 'shows global-template helper text when there is no connection message or template', () => {
globalTemplate = 'Custom global template: {title}';

const sharePostFormProps = getSharePostFormProps( {
message: '',
template: '',
} );

expect( sharePostFormProps.message ).toBe( '' );
expect( sharePostFormProps.messageHelp ).toBe( 'Global template will be used if empty.' );
} );

it( 'shows default-template helper text when global template is empty', () => {
globalTemplate = '';

const sharePostFormProps = getSharePostFormProps( {
message: '',
template: '',
} );

expect( sharePostFormProps.message ).toBe( '' );
expect( sharePostFormProps.messageHelp ).toBe(
'The default network template will be used if empty.'
);
} );

it( 'keeps current behavior when message templates feature is off', () => {
templatesEnabled = false;
globalMessage = 'Global custom message';

const sharePostFormProps = getSharePostFormProps( {
message: undefined,
template: '{title} {url}',
} );

expect( sharePostFormProps.message ).toBe( 'Global custom message' );
expect( sharePostFormProps.messageHelp ).toBeUndefined();
} );
} );
Original file line number Diff line number Diff line change
@@ -1,17 +1,33 @@
import { useDispatch } from '@wordpress/data';
import { siteHasFeature } from '@automattic/jetpack-script-data';
import { useDispatch, useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { useCallback, useMemo } from 'react';
import useFeaturedImage from '../../../hooks/use-featured-image';
import useMediaDetails from '../../../hooks/use-media-details';
import { computeAttachedMediaForSource } from '../../../hooks/use-per-network-customization/utils';
import { usePostMeta } from '../../../hooks/use-post-meta';
import { store as socialStore } from '../../../social-store';
import { Connection } from '../../../social-store/types';
import { features } from '../../../utils/constants';
import { SharePostForm, SharePostFormProps } from '../../form/share-post-form';

type PerNetworkCustomizationFormProps = {
connection: Connection;
};

const CONNECTION_TEMPLATE_HELP = __(
'Connection template will be used if empty.',
'jetpack-publicize-pkg'
);
const GLOBAL_TEMPLATE_HELP = __(
'Global template will be used if empty.',
'jetpack-publicize-pkg'
);
const DEFAULT_NETWORK_TEMPLATE_HELP = __(
'The default network template will be used if empty.',
'jetpack-publicize-pkg'
);

/**
* Per-Network Customization Form component.
*
Expand All @@ -20,19 +36,43 @@ type PerNetworkCustomizationFormProps = {
*/
export function PerNetworkCustomizationForm( { connection }: PerNetworkCustomizationFormProps ) {
const { customizeConnectionById } = useDispatch( socialStore );
const templatesEnabled = siteHasFeature( features.MESSAGE_TEMPLATES );
const {
attachedMedia: globalAttachedMedia,
shareMessage: globalMessage,
mediaSource: globalMediaSource,
} = usePostMeta();
const globalMessageTemplate = useSelect(
select => select( socialStore ).getSocialSettings().messageTemplate,
[]
);

// Get featured image details for forced attachment
const featuredImageId = useFeaturedImage();
const [ featuredImageDetails ] = useMediaDetails( featuredImageId );
const featuredImageUrl = featuredImageDetails?.mediaData?.sourceUrl;
const featuredImageMime = featuredImageDetails?.metaData?.mime ?? 'image/jpeg';

const message = connection.message ?? globalMessage ?? '';
const hasConnectionMessage = connection.message !== undefined && connection.message !== '';
const hasConnectionTemplate = Boolean( connection.template );
const hasGlobalMessageTemplate = Boolean( globalMessageTemplate );

let message = connection.message ?? globalMessage ?? '';
let fallbackHelp: string | undefined;

if ( templatesEnabled ) {
message = hasConnectionMessage
? connection.message ?? ''
: connection.template ?? globalMessage ?? '';

if ( hasConnectionTemplate ) {
fallbackHelp = CONNECTION_TEMPLATE_HELP;
} else if ( hasGlobalMessageTemplate ) {
fallbackHelp = GLOBAL_TEMPLATE_HELP;
} else {
fallbackHelp = DEFAULT_NETWORK_TEMPLATE_HELP;
}
}

// Don't default to 'none' - let undefined trigger featured image fallback detection
const mediaSource = connection.media_source ?? globalMediaSource;
Expand Down Expand Up @@ -120,6 +160,7 @@ export function PerNetworkCustomizationForm( { connection }: PerNetworkCustomiza
isInsideNavigatorModal
disabled={ ! connection.enabled }
message={ message }
messageHelp={ fallbackHelp }
onMessageChange={ handleMessageChange }
attachedMedia={ attachedMedia }
onMediaChange={ handleMediaChange }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Disabled, useNavigator } from '@wordpress/components';
import { useDispatch } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import clsx from 'clsx';
import { useCallback, type FC } from 'react';
import { useCallback, type FC, type ReactNode } from 'react';
import useSocialMediaMessage from '../../hooks/use-social-media-message';
import { store as socialStore } from '../../social-store';
import { hasSocialPaidFeatures } from '../../utils';
Expand Down Expand Up @@ -37,6 +37,16 @@ export type SharePostFormProps = {
*/
onMessageChange?: ( message: string ) => void;

/**
* Optional placeholder for the message field.
*/
messagePlaceholder?: string;

/**
* Optional help text for the message field.
*/
messageHelp?: ReactNode;

/**
* Optional attached media array. In controlled mode (when `onMediaChange` is provided),
* this value is passed to child components instead of fetching from the store.
Expand Down Expand Up @@ -88,6 +98,8 @@ export const SharePostForm: FC< SharePostFormProps > = ( {
isInsideNavigatorModal,
message: messageProp,
onMessageChange,
messagePlaceholder,
messageHelp,
attachedMedia,
imageGeneratorSettings,
mediaSource,
Expand Down Expand Up @@ -134,6 +146,8 @@ export const SharePostForm: FC< SharePostFormProps > = ( {
maxLength={ maxLength }
onChange={ updateMessage }
message={ message }
placeholder={ messagePlaceholder }
help={ messageHelp }
analyticsData={ analyticsData }
disabled={ disabled }
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useCallback, useRef } from 'react';
import { features } from '../../utils/constants';
import PlaceholdersHelp from '../placeholders-help';
import styles from './styles.module.scss';
import type { ReactNode } from 'react';

export const getPlaceholderText = () =>
__(
Expand Down Expand Up @@ -34,6 +35,9 @@ export type MessageBoxControlProps = {
/** The placeholder text for the message box */
placeholder?: string;

/** Optional help text override */
help?: ReactNode;

/** The message to display */
message: string;

Expand Down Expand Up @@ -63,6 +67,7 @@ export type MessageBoxControlProps = {
export default function MessageBoxControl( {
label = getDefaultLabel(),
placeholder,
help: helpProp,
message = '',
onChange,
disabled,
Expand Down Expand Up @@ -95,27 +100,29 @@ export default function MessageBoxControl( {
// Skip both the maxLength cap and the "characters remaining" counter, and instead
// wire the help slot to a placeholder-aware description that screen readers will
// announce via aria-describedby (which BaseControl sets on the textarea).
const help = templatesEnabled
? createInterpolateElement(
__(
'Supports placeholders like <title/> and <url/>. See the list below for all the options.',
'jetpack-publicize-pkg'
),
{
title: <code>{ '{title}' }</code>,
url: <code>{ '{url}' }</code>,
}
)
: sprintf(
/* translators: %d: the number of characters remaining. */
_n(
'%d character remaining',
'%d characters remaining',
charactersRemaining,
'jetpack-publicize-pkg'
),
charactersRemaining
);
const help =
helpProp ??
( templatesEnabled
? createInterpolateElement(
__(
'Supports placeholders like <title/> and <url/>. See the list below for all the options.',
'jetpack-publicize-pkg'
),
{
title: <code>{ '{title}' }</code>,
url: <code>{ '{url}' }</code>,
}
)
: sprintf(
/* translators: %d: the number of characters remaining. */
_n(
'%d character remaining',
'%d characters remaining',
charactersRemaining,
'jetpack-publicize-pkg'
),
charactersRemaining
) );

return (
<div className={ styles[ 'message-box-control' ] }>
Expand Down
Loading
Loading