Skip to content
13 changes: 9 additions & 4 deletions includes/Admin/Options.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ final class Options {
* @var array<string, mixed>
*/
private const DEFAULTS = [
'publication_id' => '',
'post_template_id' => '',
'publication_id' => '',
'post_template_id' => '',
'require_template_choice' => false,
];

/**
Expand Down Expand Up @@ -81,15 +82,19 @@ public static function sanitize( $input ): array {
? sanitize_text_field( (string) $input['post_template_id'] )
: '';

// An unchecked checkbox is missing from the form, so absence means off.
$require_template_choice = ! empty( $input['require_template_choice'] );

// Saving settings clears the cached template list for the selected
// publication so the dropdown reflects any beehiiv-side changes.
if ( '' !== $publication_id ) {
Cache::delete_post_templates( $publication_id );
}

return [
'publication_id' => $publication_id,
'post_template_id' => $post_template_id,
'publication_id' => $publication_id,
'post_template_id' => $post_template_id,
'require_template_choice' => $require_template_choice,
];
}
}
40 changes: 40 additions & 0 deletions includes/Admin/Registrar.php
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,17 @@ private static function register_page_settings_section( string $page_slug ): voi
'label_for' => 'beehiiv_post_template_id',
]
);

add_settings_field(
'beehiiv_require_template_choice',
__( 'Template choice', 'beehiiv' ),
[ self::class, 'render_require_template_choice_field' ],
$page_slug,
self::PAGE_SETTINGS_SECTION_ID,
[
'label_for' => 'beehiiv_require_template_choice',
]
);
}

/**
Expand Down Expand Up @@ -244,4 +255,33 @@ class="button button-secondary beehiiv-refresh-button"
</p>
<?php
}

/**
* Require-template-choice checkbox field.
*
* @since 1.2.0
*/
public static function render_require_template_choice_field(): void {
$settings = Options::get();
$name = Config::OPTION_NAME . '[require_template_choice]';
?>
<input type="hidden" name="<?php echo esc_attr( $name ); ?>" value="0" />
<label for="beehiiv_require_template_choice">
<input
type="checkbox"
id="beehiiv_require_template_choice"
name="<?php echo esc_attr( $name ); ?>"
value="1"
<?php checked( ! empty( $settings['require_template_choice'] ) ); ?>
/>
<?php esc_html_e( 'Require editors to choose a post template', 'beehiiv' ); ?>
</label>
<p class="description">
<?php
// phpcs:ignore Generic.Files.LineLength.MaxExceeded,Generic.Files.LineLength.TooLong -- Single string for translators / i18n tools.
esc_html_e( 'When on, editors must pick a template for each new post. The default template is not applied automatically to those posts, and sending is blocked until a template is chosen.', 'beehiiv' );
?>
</p>
<?php
}
}
1 change: 1 addition & 0 deletions includes/Editor/PostSettings.php
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,7 @@ private static function get_editor_config(): array {
'hasPostTemplate' => '' !== trim( (string) ( $settings['post_template_id'] ?? '' ) ),
'publicationId' => trim( (string) ( $settings['publication_id'] ?? '' ) ),
'defaultPostTemplateId' => trim( (string) ( $settings['post_template_id'] ?? '' ) ),
'requireTemplateChoice' => ! empty( $settings['require_template_choice'] ),
'canPublishPosts' => current_user_can( 'publish_posts' ),
'defaultPublicationId' => $default_pub_id,
'publications' => $is_connected ? PublicationResolver::get_editor_publications() : [],
Expand Down
28 changes: 28 additions & 0 deletions includes/Newsletter/PostSettingsBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,13 @@ public static function get_post_settings( int $post_id, bool $for_update = false

$post_template_id = self::resolve_post_template_id( $post_id, $publication_id );

if ( '' === $post_template_id && self::is_template_choice_required( $post_id ) ) {
return new WP_Error(
'beehiiv_post_template_choice_required',
sprintf( 'An editor must choose a post template before post ID: %d is sent.', $post_id )
);
}

if ( '' === $post_template_id && PublicationResolver::get_default_publication_id() !== $publication_id ) {
return new WP_Error(
'beehiiv_post_template_required',
Expand Down Expand Up @@ -457,13 +464,34 @@ public static function resolve_post_template_id( int $post_id, string $publicati
return $post_template_id;
}

// With the required-template rule on, a never-sent post gets no automatic template.
if ( self::is_template_choice_required( $post_id ) ) {
return '';
}

if ( '' !== $publication_id && PublicationResolver::get_default_publication_id() === $publication_id ) {
return self::get_site_post_template_id();
}

return '';
}

/**
* Whether an editor must choose this post's template before it can be sent.
*
* True when the "require template choice" setting is on and the post has never
* been sent to beehiiv. Applies to every user role.
*
* @param int $post_id Post ID.
* @return bool
* @since x.x.x
*/
public static function is_template_choice_required( int $post_id ): bool {
$settings = Options::get();

return ! empty( $settings['require_template_choice'] ) && ! Sender::has_beehiiv_post_id( $post_id );
}

/**
* Email subject line for the post: its newsletter title, or the post title when empty.
*
Expand Down
2 changes: 2 additions & 0 deletions includes/Newsletter/Sender.php
Original file line number Diff line number Diff line change
Expand Up @@ -1017,6 +1017,8 @@ public static function format_save_error_message( \WP_Error $error ): string {
'Choose a default post template in <a>beehiiv settings</a>, or pick one for this post in the beehiiv sidebar.',
'beehiiv'
);
case 'beehiiv_post_template_choice_required':
return __( 'Choose a post template before sending this post to beehiiv.', 'beehiiv' );
case 'beehiiv_post_template_required':
return __( "Pick a post template for this post's publication in the beehiiv sidebar.", 'beehiiv' );
case 'beehiiv_publication_missing':
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,15 @@ function PostPublicationNotices( { beehiivMeta, postPublication } ) {

if ( missingTemplate ) {
return (
<PostSettingsNotice status="warning">
{ __( 'Pick a post template for this post.', 'beehiiv' ) }
<PostSettingsNotice
status={ postPublication.requireChoice ? 'error' : 'warning' }
>
{ postPublication.requireChoice
? __(
'Choose a post template before sending this post to beehiiv.',
'beehiiv'
)
: __( 'Pick a post template for this post.', 'beehiiv' ) }
</PostSettingsNotice>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,24 @@ import { useBeehiivEditorConfig } from '../hooks/use-beehiiv-editor-config';
* @param {string} props.publicationId Publication whose templates are listed.
* @param {boolean} props.isDefaultPublication Whether that is the site-wide default publication.
* @param {boolean} [props.disabled] Whether the choice is locked.
* @param {boolean} [props.requireChoice] Whether the editor must choose a template (no automatic default).
*/
export default function NewsletterTemplateSelect( {
value,
onChange,
publicationId,
isDefaultPublication,
disabled = false,
requireChoice = false,
} ) {
const { defaultPostTemplateId: siteDefaultPostTemplateId } =
useBeehiivEditorConfig();
// Only the site-wide default publication has a default template.
const defaultPostTemplateId = isDefaultPublication
? siteDefaultPostTemplateId
: '';
// Only the site-wide default publication has a default template, and
// it is not applied automatically when the editor must choose.
const defaultPostTemplateId =
isDefaultPublication && ! requireChoice
? siteDefaultPostTemplateId
: '';
const [ templates, setTemplates ] = useState( [] );
const [ isLoading, setIsLoading ] = useState( false );
const [ justRefreshed, setJustRefreshed ] = useState( false );
Expand Down Expand Up @@ -144,21 +148,73 @@ export default function NewsletterTemplateSelect( {
}

if ( value && ! opts.some( ( option ) => option.value === value ) ) {
// Only call it unavailable once the list has loaded with templates in it,
// so a slow or failed fetch does not mislabel a good template.
const isUnavailable = ! isLoading && templates.length > 0;

opts.push( {
value,
label: value,
label: isUnavailable
? sprintf(
/* translators: %s: post template ID */
__( '%s (unavailable)', 'beehiiv' ),
value
)
: value,
} );
}

if ( ! selectedValue ) {
opts.unshift( {
value: '',
label: __( 'Select a template', 'beehiiv' ),
label: requireChoice
? __( 'Choose a template', 'beehiiv' )
: __( 'Select a template', 'beehiiv' ),
} );
}

return opts;
}, [ templates, value, selectedValue, defaultPostTemplateId ] );
}, [
templates,
value,
selectedValue,
defaultPostTemplateId,
requireChoice,
isLoading,
] );

const getHelpText = () => {
if ( disabled ) {
return __(
"The template can't be changed once the newsletter is in beehiiv.",
'beehiiv'
);
}

if ( requireChoice && ! selectedValue ) {
return __(
'Choose a post template before sending this post to beehiiv.',
'beehiiv'
);
}

if ( ! value && defaultPostTemplateId ) {
const defaultTemplate = templates.find(
( item ) => item?.id === defaultPostTemplateId
);

return sprintf(
/* translators: %s: default post template name */
__(
'This post uses the default template, "%s", unless you choose another.',
'beehiiv'
),
defaultTemplate?.name || defaultPostTemplateId
);
}

return undefined;
};

if ( isLoading && options.length === 0 ) {
return (
Expand All @@ -174,19 +230,13 @@ export default function NewsletterTemplateSelect( {

return (
<div className="beehiiv-newsletter-template">
{ isLoading && <Spinner /> }
<SelectControl
label={ __( 'Post template', 'beehiiv' ) }
value={ selectedValue }
options={ options }
onChange={ onChange }
help={
disabled
? __(
"The template can't be changed once the newsletter is in beehiiv.",
'beehiiv'
)
: undefined
}
help={ getHelpText() }
disabled={ disabled || isLoading }
__nextHasNoMarginBottom
/>
Expand Down
5 changes: 5 additions & 0 deletions src/js/editor/post-settings/css/_sidebar.scss
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@
);
}

// Also applies in the pre-publish panel, outside the sidebar content wrapper.
.beehiiv-post-settings-notice {
margin-top: 12px;
}

.beehiiv-post-settings-content {
/* stylelint-disable selector-class-pattern -- @wordpress/components */
.components-panel__body {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const DEFAULT_CONFIG = {
hasPostTemplate: false,
publicationId: '',
defaultPostTemplateId: '',
requireTemplateChoice: false,
canPublishPosts: false,
defaultPublicationId: '',
publications: [],
Expand Down
26 changes: 21 additions & 5 deletions src/js/editor/post-settings/hooks/use-post-publication.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { useEditorPublications } from './use-editor-publications';
* @property {boolean} isDefaultPublication Whether the effective publication is the default.
* @property {boolean} isFallback Whether the saved publication is no longer connected.
* @property {boolean} missingPublication Whether no publication can be used yet.
* @property {boolean} requireChoice Whether an editor must choose this post's template.
* @property {boolean} missingTemplate Whether the post still needs a template.
* @property {boolean} isPostReady Whether the post has a publication and a template.
*/
Expand All @@ -25,8 +26,11 @@ import { useEditorPublications } from './use-editor-publications';
* @return {PostPublication} Per-post publication state.
*/
export function usePostPublication( beehiivMeta ) {
const { defaultPublicationId, defaultPostTemplateId } =
useBeehiivEditorConfig();
const {
defaultPublicationId,
defaultPostTemplateId,
requireTemplateChoice,
} = useBeehiivEditorConfig();
const publications = useEditorPublications().filter( ( item ) => item?.id );
const storedPublicationId = beehiivMeta?.beehiivPublicationId || '';
const templateId = beehiivMeta?.beehiivPostTemplateId || '';
Expand All @@ -45,10 +49,21 @@ export function usePostPublication( beehiivMeta ) {
!! effectivePublicationId &&
effectivePublicationId === defaultPublicationId;

// With the required-template setting on, a never-sent post has no automatic template.
const requireChoice =
requireTemplateChoice && ! beehiivMeta?.newsletterAlreadySent;

// On fallback the server uses the default template only.
const hasTemplate = isFallback
? !! defaultPostTemplateId
: !! templateId || ( isDefaultPublication && !! defaultPostTemplateId );
let hasTemplate;
if ( requireChoice ) {
hasTemplate = ! isFallback && !! templateId;
} else if ( isFallback ) {
hasTemplate = !! defaultPostTemplateId;
} else {
hasTemplate =
!! templateId ||
( isDefaultPublication && !! defaultPostTemplateId );
}

return {
publications,
Expand All @@ -57,6 +72,7 @@ export function usePostPublication( beehiivMeta ) {
effectivePublicationId,
isDefaultPublication,
isFallback,
requireChoice,
missingPublication: ! effectivePublicationId,
missingTemplate: !! effectivePublicationId && ! hasTemplate,
isPostReady: !! effectivePublicationId && hasTemplate,
Expand Down
Loading
Loading