feat: render admin settings workflow

This commit is contained in:
Keith Solomon
2026-05-07 06:36:01 -05:00
parent c84df8b5f2
commit 4d83bd4a48
2 changed files with 170 additions and 39 deletions
+143 -37
View File
@@ -10,22 +10,33 @@
if ( ! defined( 'ABSPATH' ) ) { if ( ! defined( 'ABSPATH' ) ) {
exit; exit;
} }
$sync_pairs = $settings->syncPairs();
if ( array() === $sync_pairs ) {
$sync_pairs = array(
array(
'name' => '',
'source_url' => '',
'destination_url' => '',
'username' => '',
'application_password' => '',
'default_direction' => 'push',
'content_types' => array( 'posts', 'terms', 'media', 'custom_post_types' ),
'url_mappings' => array(
array(
'source' => '',
'destination' => '',
),
),
),
);
}
?> ?>
<div class="wrap"> <div class="wrap">
<h1><?php echo esc_html__( 'WP Content Sync', 'wp-content-sync' ); ?></h1> <h1><?php echo esc_html__( 'WP Content Sync', 'wp-content-sync' ); ?></h1>
<p><?php echo esc_html__( 'Configure sync pairs, test connections, and run push or pull operations from this screen.', 'wp-content-sync' ); ?></p> <p><?php echo esc_html__( 'Configure sync pairs, test connections, and run push or pull operations from this screen.', 'wp-content-sync' ); ?></p>
<div class="notice notice-info">
<p>
<?php
echo esc_html__(
'The foundation is installed. Sync pair management, URL mappings, file transport, and REST transport will be added in upcoming implementation phases.',
'wp-content-sync'
);
?>
</p>
</div>
<?php // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Displays redirect status only. ?> <?php // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Displays redirect status only. ?>
<?php if ( isset( $_GET['wpcs_import_error'] ) ) : ?> <?php if ( isset( $_GET['wpcs_import_error'] ) ) : ?>
<div class="notice notice-error"> <div class="notice notice-error">
@@ -40,35 +51,130 @@ if ( ! defined( 'ABSPATH' ) ) {
</div> </div>
<?php endif; ?> <?php endif; ?>
<h2><?php echo esc_html__( 'Current Defaults', 'wp-content-sync' ); ?></h2> <h2><?php echo esc_html__( 'Configuration', 'wp-content-sync' ); ?></h2>
<table class="widefat striped"> <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
<tbody> <input type="hidden" name="action" value="wpcs_save_settings" />
<tr> <?php wp_nonce_field( 'wpcs_save_settings', 'wpcs_settings_nonce' ); ?>
<th scope="row"><?php echo esc_html__( 'Configured Sync Pairs', 'wp-content-sync' ); ?></th>
<td><?php echo esc_html( (string) count( $settings->syncPairs() ) ); ?></td> <h3><?php echo esc_html__( 'Sync Pairs', 'wp-content-sync' ); ?></h3>
</tr> <?php foreach ( $sync_pairs as $index => $pair ) : ?>
<tr> <div class="card">
<th scope="row"><?php echo esc_html__( 'Logging Level', 'wp-content-sync' ); ?></th> <h4>
<td><?php echo esc_html( $settings->loggingLevel() ); ?></td>
</tr>
<tr>
<th scope="row"><?php echo esc_html__( 'URL Replacement', 'wp-content-sync' ); ?></th>
<td>
<?php <?php
echo esc_html( printf(
$settings->automaticUrlReplacementEnabled() /* translators: %d: sync pair number. */
? __( 'Enabled', 'wp-content-sync' ) esc_html__( 'Sync Pair %d', 'wp-content-sync' ),
: __( 'Disabled', 'wp-content-sync' ) (int) $index + 1
); );
?> ?>
</td> </h4>
</tr> <table class="form-table" role="presentation">
<tr> <tbody>
<th scope="row"><?php echo esc_html__( 'Conflict Strategy', 'wp-content-sync' ); ?></th> <tr>
<td><?php echo esc_html( $settings->conflictStrategy() ); ?></td> <th scope="row"><label for="wpcs-pair-name-<?php echo esc_attr( (string) $index ); ?>"><?php echo esc_html__( 'Name', 'wp-content-sync' ); ?></label></th>
</tr> <td><input class="regular-text" id="wpcs-pair-name-<?php echo esc_attr( (string) $index ); ?>" name="wpcs_settings[sync_pairs][<?php echo esc_attr( (string) $index ); ?>][name]" type="text" value="<?php echo esc_attr( $pair['name'] ); ?>" /></td>
</tbody> </tr>
</table> <tr>
<th scope="row"><label for="wpcs-source-url-<?php echo esc_attr( (string) $index ); ?>"><?php echo esc_html__( 'Source URL', 'wp-content-sync' ); ?></label></th>
<td><input class="regular-text code" id="wpcs-source-url-<?php echo esc_attr( (string) $index ); ?>" name="wpcs_settings[sync_pairs][<?php echo esc_attr( (string) $index ); ?>][source_url]" type="url" value="<?php echo esc_attr( $pair['source_url'] ); ?>" /></td>
</tr>
<tr>
<th scope="row"><label for="wpcs-destination-url-<?php echo esc_attr( (string) $index ); ?>"><?php echo esc_html__( 'Destination URL', 'wp-content-sync' ); ?></label></th>
<td><input class="regular-text code" id="wpcs-destination-url-<?php echo esc_attr( (string) $index ); ?>" name="wpcs_settings[sync_pairs][<?php echo esc_attr( (string) $index ); ?>][destination_url]" type="url" value="<?php echo esc_attr( $pair['destination_url'] ); ?>" /></td>
</tr>
<tr>
<th scope="row"><label for="wpcs-username-<?php echo esc_attr( (string) $index ); ?>"><?php echo esc_html__( 'REST Username', 'wp-content-sync' ); ?></label></th>
<td><input class="regular-text" id="wpcs-username-<?php echo esc_attr( (string) $index ); ?>" name="wpcs_settings[sync_pairs][<?php echo esc_attr( (string) $index ); ?>][username]" type="text" value="<?php echo esc_attr( $pair['username'] ); ?>" /></td>
</tr>
<tr>
<th scope="row"><label for="wpcs-application-password-<?php echo esc_attr( (string) $index ); ?>"><?php echo esc_html__( 'Application Password', 'wp-content-sync' ); ?></label></th>
<td>
<input class="regular-text" id="wpcs-application-password-<?php echo esc_attr( (string) $index ); ?>" name="wpcs_settings[sync_pairs][<?php echo esc_attr( (string) $index ); ?>][application_password]" type="password" value="" autocomplete="new-password" placeholder="<?php echo esc_attr( __( 'Enter a new application password to replace the saved value', 'wp-content-sync' ) ); ?>" />
<p class="description"><?php echo esc_html__( 'Saved application passwords are never rendered back to the browser.', 'wp-content-sync' ); ?></p>
</td>
</tr>
<tr>
<th scope="row"><?php echo esc_html__( 'Default Direction', 'wp-content-sync' ); ?></th>
<td>
<select name="wpcs_settings[sync_pairs][<?php echo esc_attr( (string) $index ); ?>][default_direction]">
<option value="push" <?php echo 'push' === $pair['default_direction'] ? 'selected="selected"' : ''; ?>><?php echo esc_html__( 'Push', 'wp-content-sync' ); ?></option>
<option value="pull" <?php echo 'pull' === $pair['default_direction'] ? 'selected="selected"' : ''; ?>><?php echo esc_html__( 'Pull', 'wp-content-sync' ); ?></option>
</select>
</td>
</tr>
<tr>
<th scope="row"><?php echo esc_html__( 'Content Types', 'wp-content-sync' ); ?></th>
<td>
<?php foreach ( array( 'posts', 'terms', 'media', 'custom_post_types' ) as $content_type ) : ?>
<label>
<input name="wpcs_settings[sync_pairs][<?php echo esc_attr( (string) $index ); ?>][content_types][]" type="checkbox" value="<?php echo esc_attr( $content_type ); ?>" <?php echo in_array( $content_type, $pair['content_types'], true ) ? 'checked="checked"' : ''; ?> />
<?php echo esc_html( ucwords( str_replace( '_', ' ', $content_type ) ) ); ?>
</label><br />
<?php endforeach; ?>
</td>
</tr>
<tr>
<th scope="row"><?php echo esc_html__( 'URL Mapping', 'wp-content-sync' ); ?></th>
<td>
<?php $mapping = $pair['url_mappings'][0] ?? array( 'source' => '', 'destination' => '' ); ?>
<input class="regular-text code" name="wpcs_settings[sync_pairs][<?php echo esc_attr( (string) $index ); ?>][url_mappings][0][source]" type="url" value="<?php echo esc_attr( $mapping['source'] ); ?>" placeholder="<?php echo esc_attr( __( 'Source URL', 'wp-content-sync' ) ); ?>" />
<input class="regular-text code" name="wpcs_settings[sync_pairs][<?php echo esc_attr( (string) $index ); ?>][url_mappings][0][destination]" type="url" value="<?php echo esc_attr( $mapping['destination'] ); ?>" placeholder="<?php echo esc_attr( __( 'Destination URL', 'wp-content-sync' ) ); ?>" />
</td>
</tr>
</tbody>
</table>
</div>
<?php endforeach; ?>
<h3><?php echo esc_html__( 'Defaults', 'wp-content-sync' ); ?></h3>
<table class="form-table" role="presentation">
<tbody>
<tr>
<th scope="row"><?php echo esc_html__( 'Logging Level', 'wp-content-sync' ); ?></th>
<td>
<select name="wpcs_settings[logging_level]">
<?php foreach ( array( 'error', 'warning', 'info', 'debug' ) as $level ) : ?>
<option value="<?php echo esc_attr( $level ); ?>" <?php echo $level === $settings->loggingLevel() ? 'selected="selected"' : ''; ?>><?php echo esc_html( $level ); ?></option>
<?php endforeach; ?>
</select>
</td>
</tr>
<tr>
<th scope="row"><?php echo esc_html__( 'Conflict Strategy', 'wp-content-sync' ); ?></th>
<td>
<select name="wpcs_settings[conflict_strategy]">
<option value="last_write_wins" <?php echo 'last_write_wins' === $settings->conflictStrategy() ? 'selected="selected"' : ''; ?>><?php echo esc_html__( 'Last write wins', 'wp-content-sync' ); ?></option>
<option value="manual_review" <?php echo 'manual_review' === $settings->conflictStrategy() ? 'selected="selected"' : ''; ?>><?php echo esc_html__( 'Manual review', 'wp-content-sync' ); ?></option>
</select>
</td>
</tr>
<tr>
<th scope="row"><?php echo esc_html__( 'URL Replacement', 'wp-content-sync' ); ?></th>
<td>
<label>
<input name="wpcs_settings[automatic_url_replacement]" type="checkbox" value="1" <?php echo $settings->automaticUrlReplacementEnabled() ? 'checked="checked"' : ''; ?> />
<?php echo esc_html__( 'Automatically replace mapped URLs during import.', 'wp-content-sync' ); ?>
</label>
</td>
</tr>
<tr>
<th scope="row"><label for="wpcs-log-retention"><?php echo esc_html__( 'Log Retention', 'wp-content-sync' ); ?></label></th>
<td><input id="wpcs-log-retention" name="wpcs_settings[log_retention]" type="number" min="10" max="1000" value="<?php echo esc_attr( (string) $settings->logRetention() ); ?>" /></td>
</tr>
<tr>
<th scope="row"><?php echo esc_html__( 'Debug Logging', 'wp-content-sync' ); ?></th>
<td>
<label>
<input name="wpcs_settings[debug_logging]" type="checkbox" value="1" <?php echo $settings->debugLoggingEnabled() ? 'checked="checked"' : ''; ?> />
<?php echo esc_html__( 'Enable verbose debug logs while troubleshooting.', 'wp-content-sync' ); ?>
</label>
</td>
</tr>
</tbody>
</table>
<?php submit_button( __( 'Save Settings', 'wp-content-sync' ) ); ?>
</form>
<h2><?php echo esc_html__( 'File Package Import', 'wp-content-sync' ); ?></h2> <h2><?php echo esc_html__( 'File Package Import', 'wp-content-sync' ); ?></h2>
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" enctype="multipart/form-data"> <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" enctype="multipart/form-data">
+27 -2
View File
@@ -30,8 +30,33 @@ class DashboardTemplateTest extends TestCase {
self::assertStringContainsString( 'The package JSON file was validated successfully.', $output ); self::assertStringContainsString( 'The package JSON file was validated successfully.', $output );
} }
private function renderDashboard(): string { public function test_it_renders_settings_form_with_nonce_and_escaped_pair_values(): void {
$settings = Settings::fromArray( array() ); $settings = Settings::fromArray(
array(
'sync_pairs' => array(
array(
'name' => '<b>Staging</b>',
'source_url' => 'https://example.test',
'destination_url' => 'https://staging.example.test',
'username' => 'codex',
),
),
)
);
$output = $this->renderDashboard( $settings );
self::assertStringContainsString( 'action="https://example.test/wp-admin/admin-post.php"', $output );
self::assertStringContainsString( 'name="action" value="wpcs_save_settings"', $output );
self::assertStringContainsString( 'name="wpcs_settings_nonce"', $output );
self::assertStringContainsString( 'Staging', $output );
self::assertStringNotContainsString( '<b>Staging</b>', $output );
self::assertStringContainsString( 'name="wpcs_settings[sync_pairs][0][application_password]"', $output );
self::assertStringContainsString( 'name="wpcs_settings[sync_pairs][0][url_mappings][0][source]"', $output );
}
private function renderDashboard( ?Settings $settings = null ): string {
$settings = $settings ?? Settings::fromArray( array() );
ob_start(); ob_start();
include WPCS_PLUGIN_DIR . 'templates/admin/dashboard.php'; include WPCS_PLUGIN_DIR . 'templates/admin/dashboard.php';