Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
314fedb
feat: add version source and package installer seams for the version …
lightbulbman Sep 22, 2026
a59178d
refactor: resolve the version switcher source and installer through f…
lightbulbman Sep 22, 2026
561c7c6
feat: refetch the available plugin versions when resetting caches
lightbulbman Sep 22, 2026
a5866c5
fix: discard the cached version catalogue on upgrade
lightbulbman Sep 22, 2026
03640e1
fix: render the version switcher notice when no switcher config is pr…
lightbulbman Sep 22, 2026
711b76b
chore: add tests for the version switcher seams
lightbulbman Sep 22, 2026
9dd96a2
fix: remove the version switcher transients on complete uninstall
lightbulbman Sep 22, 2026
ac268c5
docs: note the version switcher seams and cache changes in the changelog
lightbulbman Sep 22, 2026
e2e6e93
fix: keep the message behind a failed version catalogue request
lightbulbman Sep 22, 2026
a87da72
fix: explain an empty version catalogue instead of disabling the swit…
lightbulbman Sep 22, 2026
97d7174
fix: only render the version floor notice when the source reports a f…
lightbulbman Sep 22, 2026
1685b60
fix: record a refused package request the same way as a refused catal…
lightbulbman Oct 14, 2026
1bf8716
feat: hide the refresh versions button while versions cannot be listed
lightbulbman Oct 14, 2026
1cb5819
fix: render the version switch warning inside the switcher instead of…
lightbulbman Oct 14, 2026
3220e2c
feat: let a version source attach a warning to a version
lightbulbman Oct 14, 2026
56163c8
fix: show the refreshing label while the version list refreshes
lightbulbman Oct 14, 2026
dd10964
chore: drop an inaccurate comment from the version switch script
lightbulbman Oct 14, 2026
8fdc77c
feat: give the version switch warning a yellow background
lightbulbman Oct 14, 2026
7be664e
chore: add tests for version warnings and the refresh button
lightbulbman Oct 14, 2026
ae74ede
Merge branch 'core-beta' into refactor/version-switcher-seams/core
lightbulbman Oct 14, 2026
7e6f8dd
Merge branch 'core-beta' into refactor/version-switcher-seams/core
lightbulbman Oct 19, 2026
d8457c2
Merge remote-tracking branch 'origin/core-beta' into refactor/version…
lightbulbman Oct 19, 2026
809691d
fix: resolve the version switcher source each time it is used
lightbulbman Oct 19, 2026
2e2b55d
fix: log why a version switch install failed
lightbulbman Oct 19, 2026
e8ad704
fix: clear the WordPress.org version list along with the active source's
lightbulbman Oct 19, 2026
9423e97
fix: report a failed version list refresh instead of success
lightbulbman Oct 19, 2026
0586509
chore: drop support for an error transient shape that was never stored
lightbulbman Oct 19, 2026
d610bae
fix: show version switch messages as text and keep them visible
lightbulbman Oct 19, 2026
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
1 change: 1 addition & 0 deletions src/css/common/_theme.scss
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
--cs-color-surface-subtle: #f6f7f7;
--cs-color-surface-muted: #f0f0f1;
--cs-color-surface-info: #f0f6fc;
--cs-color-surface-warning: #f2ebc3;
--cs-color-border: #c3c4c7;
--cs-color-border-subtle: #dcdcde;
--cs-color-info: #72aee6;
Expand Down
8 changes: 2 additions & 6 deletions src/css/settings.scss
Original file line number Diff line number Diff line change
Expand Up @@ -196,15 +196,11 @@ body.js {
margin-block-start: 20px !important;
padding: 12px 16px;
border-inline-start: 4px solid var(--cs-color-warning);
background: var(--cs-color-surface-subtle);
background: var(--cs-color-surface-warning);

p {
margin: 0;
color: #8f6914;

strong {
color: #8f6914;
}
color: var(--cs-color-text);
}
}

Expand Down
4 changes: 0 additions & 4 deletions src/js/services/settings/tabs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,6 @@ const selectTab = (tabsWrapper: Element, tab: Element, section: string) => {
document.querySelectorAll('.cloud-message').forEach(element => {
element.classList.add('hidden')
})

if ('version-switch' !== section) {
document.getElementById('version-switch-warning')?.classList.add('hidden')
}
}

// Refresh the editor preview whenever tabs change. CodeMirror measures itself
Expand Down
39 changes: 25 additions & 14 deletions src/js/services/settings/version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,17 @@ const bindDropdown = (
currentVersion: string
): void => {
const warningNotice = document.getElementById('version-switch-warning')
const targetWarning = document.getElementById('version-switch-target-warning')

dropdown.addEventListener('change', () => {
const selectedVersion = dropdown.value
const selectedWarning = dropdown.selectedOptions.item(0)?.dataset.warning ?? ''

if (targetWarning) {
targetWarning.textContent = selectedWarning
targetWarning.hidden = !selectedWarning
}

if (!button) {
return
}
Expand All @@ -61,6 +69,16 @@ const bindDropdown = (

const SUCCESS_RELOAD_MS = 3000

const showResult = (result: HTMLElement, type: 'warning' | 'info' | 'success' | 'error', message = ''): void => {
const paragraph = document.createElement('p')
paragraph.textContent = message

// Admin notices without the plugin's own class are hidden on its screens.
result.className = `notice notice-${type} code-snippets-notice`
result.replaceChildren(paragraph)
result.style.display = ''
}

const postForm = async (data: Record<string, string>, config: VersionConfig): Promise<AjaxResponse> => {
const body = new URLSearchParams()
Object.keys(data).forEach(k => body.append(k, data[k]))
Expand Down Expand Up @@ -90,19 +108,15 @@ const bindSwitch = (
void (async (): Promise<void> => {
const targetVersion = dropdown.value
if (!targetVersion || targetVersion === currentVersion) {
result.className = 'notice notice-warning'
result.innerHTML = `<p>${i18n?.selectDifferent}</p>`
result.style.display = ''
showResult(result, 'warning', i18n?.selectDifferent)
return
}

button.disabled = true
const originalText = button.textContent ?? ''
button.textContent = i18n?.switching ?? ''

result.className = 'notice notice-info'
result.innerHTML = `<p>${i18n?.processing}</p>`
result.style.display = ''
showResult(result, 'info', i18n?.processing)

try {
const response = await postForm({
Expand All @@ -112,19 +126,16 @@ const bindSwitch = (
}, cfg)

if (response.success) {
result.className = 'notice notice-success'
result.innerHTML = `<p>${response.data?.message ?? ''}</p>`
showResult(result, 'success', response.data?.message ?? '')
setTimeout(() => window.location.reload(), SUCCESS_RELOAD_MS)
return
}

result.className = 'notice notice-error'
result.innerHTML = `<p>${response.data?.message ?? i18n?.error}</p>`
showResult(result, 'error', response.data?.message ?? i18n?.error)
button.disabled = false
button.textContent = originalText
} catch (_err) {
result.className = 'notice notice-error'
result.innerHTML = `<p>${i18n?.errorSwitch}</p>`
showResult(result, 'error', i18n?.errorSwitch)
button.disabled = false
button.textContent = originalText
}
Expand All @@ -142,7 +153,7 @@ const bindRefresh = (
void (async (): Promise<void> => {
const original = btn.textContent ?? ''
btn.disabled = true
btn.textContent = i18n?.error ?? ''
btn.textContent = i18n?.refreshing ?? ''

try {
await postForm({
Expand All @@ -169,7 +180,7 @@ export const initVersionSwitch = (): void => {
const config = window.code_snippets_version_switch

if (!config) {
throw Error('version switch config missing')
return
}

const button = <HTMLButtonElement | null> document.getElementById('switch-version-btn')
Expand Down
5 changes: 5 additions & 0 deletions src/php/Core/Uninstaller.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use Code_Snippets\Client\Feedback_Client;
use Code_Snippets\Model\Feedback_Connection;
use Code_Snippets\Search\Universal_Search;
use Code_Snippets\Settings\Version_Switch;
use function Code_Snippets\flush_versioned_cache_groups;

/**
Expand Down Expand Up @@ -96,6 +97,10 @@ private function uninstall_current_site() {
delete_option( Feedback_Connection::CREDENTIALS_OPTION );
delete_transient( Feedback_Panel::SUMMARY_TRANSIENT );
delete_transient( Feedback_Client::REGISTRATION_FAILURE_TRANSIENT );

// The switcher caches under a key belonging to whichever source is in
// force, so it has to do its own clearing rather than name the keys here.
Version_Switch::clear_version_caches();
}

/**
Expand Down
7 changes: 7 additions & 0 deletions src/php/Core/Upgrader.php
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,13 @@ private function do_site_upgrades() {
$role->remove_cap( apply_filters( 'code_snippets_cap', 'manage_snippets' ) );
}

// The version switcher caches its catalogue under a key belonging to the
// source that produced it. A site whose source changed across this upgrade
// would otherwise keep serving the previous source's list until it expired.
if ( version_compare( $prev_version, '4.0.0', '<' ) ) {
delete_transient( 'code_snippets_available_versions' );
}

if ( false === $prev_version ) {
add_action( 'init', [ $this, 'create_sample_content' ] );
}
Expand Down
27 changes: 27 additions & 0 deletions src/php/Settings/Package_Installer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

namespace Code_Snippets\Settings;

use WP_Error;

/**
* Installs a plugin package over the currently installed plugin.
*
* Register an implementation on the `code_snippets_package_installer` filter to
* replace how the version switcher writes a package to disk.
*
* @package Code_Snippets
* @subpackage Settings
*/
interface Package_Installer {

/**
* Install a plugin package.
*
* @param string $package Local path to a package, or a download URL.
*
* @return array{version: string}|WP_Error Version installed, or '' when the
* installer cannot determine it.
*/
public function install( string $package );
}
7 changes: 1 addition & 6 deletions src/php/Settings/Settings_Fields.php
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ public static function get_field_definitions(): array {
'reset_caches' => [
'name' => __( 'Reset Caches', 'code-snippets' ),
'type' => 'action',
'desc' => __( 'Use this button to manually clear snippets caches. Worth doing before switching to an older version of the plugin, if your site uses a persistent object cache.', 'code-snippets' ),
'desc' => __( 'Use this button to manually clear snippets caches and refetch the list of plugin versions available to install. Worth doing before switching to an older version of the plugin, if your site uses a persistent object cache.', 'code-snippets' ),
],
];

Expand All @@ -101,11 +101,6 @@ public static function get_field_definitions(): array {
'type' => 'callback',
'render_callback' => [ '\\Code_Snippets\\Settings\\Version_Switch', 'render_refresh_versions_field' ],
],
'version_warning' => [
'name' => '',
'type' => 'callback',
'render_callback' => [ '\\Code_Snippets\\Settings\\Version_Switch', 'render_version_switch_warning' ],
],
];

$fields['general'] = [
Expand Down
3 changes: 1 addition & 2 deletions src/php/Settings/Settings_Layout.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,6 @@ public static function get_tab_contents(): array {
[ 'permissions', 'role_capabilities' ],
[ 'version-switch', 'version_switcher' ],
[ 'version-switch', 'refresh_versions' ],
[ 'version-switch', 'version_warning' ],
[ 'debug', 'reset_caches' ],
[ 'debug', 'database_update' ],
[ 'general', 'complete_uninstall' ],
Expand Down Expand Up @@ -210,7 +209,7 @@ public static function get_descriptions(): array {
'max_revisions' => __( 'Saves a copy each time a snippet changes, so you can compare versions and roll back a bad edit.', 'code-snippets' ),
'preserve_on_delete' => __( 'Keep the revision history after a snippet is deleted, so it can still be recovered.', 'code-snippets' ),
'enable_admin_bar' => __( 'Jump to any snippet from anywhere in the admin, and see which ones ran on the page you are looking at.', 'code-snippets' ),
'reset_caches' => __( 'Clears stored snippet data. Worth doing before switching to an older version of the plugin.', 'code-snippets' ),
'reset_caches' => __( 'Clears stored snippet data and refetches the list of plugin versions available to install. Worth doing before switching to an older version of the plugin.', 'code-snippets' ),
];
}
}
120 changes: 120 additions & 0 deletions src/php/Settings/Upgrader_Package_Installer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<?php

namespace Code_Snippets\Settings;

use Plugin_Upgrader;
use WP_Ajax_Upgrader_Skin;
use WP_Error;
use WP_Upgrader_Skin;

/**
* Installs a package with WordPress's own plugin upgrader.
*
* This is the switcher's default installer. `Plugin_Upgrader::install()` accepts
* a local path as readily as a URL: `WP_Upgrader::download_package()` returns
* its argument unchanged when it is not an http(s) or FTP URL and the file
* exists.
*
* @package Code_Snippets
* @subpackage Settings
*/
class Upgrader_Package_Installer implements Package_Installer {

/**
* Install a plugin package.
*
* @param string $package Local path to a package, or a download URL.
*
* @return array{version: string}|WP_Error
*/
public function install( string $package ) {
if ( ! function_exists( 'wp_update_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/update.php';
}
if ( ! function_exists( 'show_message' ) ) {
require_once ABSPATH . 'wp-admin/includes/misc.php';
}
if ( ! class_exists( 'Plugin_Upgrader' ) ) {
require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
}

$update_handler = new WP_Ajax_Upgrader_Skin();
$upgrader = new Plugin_Upgrader( $update_handler );

$result = $upgrader->install(
$package,
[
'overwrite_package' => true,
'clear_update_cache' => true,
]
);

if ( is_wp_error( $result ) ) {
return $result;
}

if ( ! $result ) {
$messages = $this->extract_handler_messages( $update_handler, $upgrader );

return new WP_Error(
'package_install_failed',
$messages
? wp_trim_words( wp_strip_all_tags( $messages ), 40 )
: __( 'The package could not be installed.', 'code-snippets' )
);
}

// The upgrader reports nothing about what it wrote, so the caller falls
// back to the version it asked for.
return [ 'version' => '' ];
}

/**
* Extract error messages from an upgrade handler.
*
* @param WP_Upgrader_Skin|null $update_handler Update handler.
* @param Plugin_Upgrader|null $upgrader Plugin upgrader.
*
* @return string
*
* phpcs:disable WordPress.PHP.DevelopmentFunctions.error_log_print_r
*/
public function extract_handler_messages( ?WP_Upgrader_Skin $update_handler, ?Plugin_Upgrader $upgrader ): string {
$handler_messages = '';

if ( isset( $update_handler ) ) {
if ( method_exists( $update_handler, 'get_errors' ) ) {
$errs = $update_handler->get_errors();
if ( $errs instanceof WP_Error && $errs->has_errors() ) {
$handler_messages .= implode( "\n", $errs->get_error_messages() );
}
}
if ( method_exists( $update_handler, 'get_error_messages' ) ) {
$em = $update_handler->get_error_messages();
if ( $em ) {
$handler_messages .= "\n" . $em;
}
}
if ( method_exists( $update_handler, 'get_upgrade_messages' ) ) {
$upgrade_msgs = $update_handler->get_upgrade_messages();
if ( is_array( $upgrade_msgs ) ) {
$handler_messages .= "\n" . implode( "\n", $upgrade_msgs );
} elseif ( $upgrade_msgs ) {
$handler_messages .= "\n" . $upgrade_msgs;
}
}
}

if ( empty( $handler_messages ) && isset( $upgrader->result ) ) {
if ( is_wp_error( $upgrader->result ) ) {
$handler_messages = implode( "\n", $upgrader->result->get_error_messages() );
} else {
$handler_messages = is_scalar( $upgrader->result )
? (string) $upgrader->result
: print_r( $upgrader->result, true );
}
}

return trim( $handler_messages );
}
}
Loading
Loading