From 10351061200bf2cd28a382b914ce33b9c3fe1317 Mon Sep 17 00:00:00 2001 From: lightbulbman Date: Fri, 14 Mar 2025 00:33:19 +0000 Subject: [PATCH 001/318] #228 html validation bug fix --- src/php/snippet-ops.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/php/snippet-ops.php b/src/php/snippet-ops.php index 2e3a9023..6e189d32 100644 --- a/src/php/snippet-ops.php +++ b/src/php/snippet-ops.php @@ -295,10 +295,12 @@ function activate_snippet( int $id, ?bool $network = null ) { // translators: %d: snippet identifier. return sprintf( __( 'Could not locate snippet with ID %d.', 'code-snippets' ), $id ); } - - $validator = new Validator( $snippet->code ); - if ( $validator->validate() ) { - return __( 'Could not activate snippet: code did not pass validation.', 'code-snippets' ); + + if('php' == $snippet->type ){ + $validator = new Validator( $snippet->code ); + if ( $validator->validate() ) { + return __( 'Could not activate snippet: code did not pass validation.', 'code-snippets' ); + } } $result = $wpdb->update( From b2be2b83233e1c8a4689842871ef72a007909b7d Mon Sep 17 00:00:00 2001 From: Louis Wolmarans Date: Tue, 1 Jul 2025 22:43:51 +0200 Subject: [PATCH 002/318] wip --- src/php/class-db.php | 10 +++ src/php/class-plugin.php | 4 + src/php/class-snippet-files.php | 125 ++++++++++++++++++++++++++++++++ src/php/class-snippet.php | 2 +- src/php/load.php | 3 +- src/php/snippet-ops.php | 58 ++++++++++++++- 6 files changed, 197 insertions(+), 5 deletions(-) create mode 100644 src/php/class-snippet-files.php diff --git a/src/php/class-db.php b/src/php/class-db.php index 64ea5f47..a0b50782 100644 --- a/src/php/class-db.php +++ b/src/php/class-db.php @@ -283,4 +283,14 @@ function ( $snippet ) use ( $active_shared_ids ) { return $active_snippets; } + + public function get_active_tables(): array { + $active_tables = array( $this->table ); + + if ( is_multisite() ) { + $active_tables[] = $this->ms_table; + } + + return $active_tables; + } } diff --git a/src/php/class-plugin.php b/src/php/class-plugin.php index 92bebfa3..32a679db 100644 --- a/src/php/class-plugin.php +++ b/src/php/class-plugin.php @@ -119,6 +119,10 @@ public function load_plugin() { // Cloud List Table shared functions. require_once $includes_path . '/cloud/list-table-shared-ops.php'; + // Snippet files. + require_once $includes_path . '/class-snippet-files.php'; + ( new Snippet_Files() )->register_hooks(); + $this->active_snippets = new Active_Snippets(); $this->front_end = new Front_End(); $this->cloud_api = new Cloud_API(); diff --git a/src/php/class-snippet-files.php b/src/php/class-snippet-files.php new file mode 100644 index 00000000..f319b5ee --- /dev/null +++ b/src/php/class-snippet-files.php @@ -0,0 +1,125 @@ +get_type() ) { + return; + } + $base_dir = WP_CONTENT_DIR . '/code-snippets/' . $table; + + if ( ! is_dir( $base_dir ) ) { + wp_mkdir_p( $base_dir ); + } + + $file_path = trailingslashit( $base_dir ) . $snippet->id . '.php'; + + if ( $snippet->active ) { + $content = "code; + + file_put_contents( $file_path, $content ); + } else { + @unlink( $file_path ); + } + + $index_file_path = trailingslashit( $base_dir ) . 'index.php'; + + $active_snippets = is_file( $index_file_path ) ? require $index_file_path : []; + + if ( $snippet->active ) { + $active_snippets[ $snippet->id ] = $snippet->get_fields(); + } else { + unset( $active_snippets[ $snippet->id ] ); + } + + $index_content = "get_type() ) { + return; + } + + $table = code_snippets()->db->get_table_name( $network ); + + $base_dir = WP_CONTENT_DIR . '/code-snippets/' . $table; + + $file_path = trailingslashit( $base_dir ) . $snippet->id . '.php'; + + @unlink( $file_path ); + + $index_file_path = trailingslashit( $base_dir ) . 'index.php'; + + $active_snippets = is_file( $index_file_path ) ? require $index_file_path : []; + + unset( $active_snippets[ $snippet_id ] ); + + $index_content = "get_type() ) { + return; + } + + $table = code_snippets()->db->get_table_name( $network ); + + $base_dir = WP_CONTENT_DIR . '/code-snippets/' . $table; + + $file_path = trailingslashit( $base_dir ) . $snippet->id . '.php'; + + $content = "code; + + file_put_contents( $file_path, $content ); + + $index_file_path = trailingslashit( $base_dir ) . 'index.php'; + + $active_snippets = is_file( $index_file_path ) ? require $index_file_path : []; + + $active_snippets[ $snippet->id ] = $snippet->get_fields(); + + $index_content = "get_type() ) { + return; + } + + $table = code_snippets()->db->get_table_name( $network ); + + $base_dir = WP_CONTENT_DIR . '/code-snippets/' . $table; + + $file_path = trailingslashit( $base_dir ) . $snippet->id . '.php'; + + @unlink( $file_path ); + + $index_file_path = trailingslashit( $base_dir ) . 'index.php'; + + $active_snippets = is_file( $index_file_path ) ? require $index_file_path : []; + + unset( $active_snippets[ $snippet_id ] ); + + $index_content = "scope, -4 ) ) { return 'css'; } elseif ( '-js' === substr( $this->scope, -3 ) ) { diff --git a/src/php/load.php b/src/php/load.php index 2de59ce2..8700186a 100644 --- a/src/php/load.php +++ b/src/php/load.php @@ -65,4 +65,5 @@ function code_snippets(): Plugin { code_snippets()->load_plugin(); // Execute the snippets once the plugins are loaded. -add_action( 'plugins_loaded', __NAMESPACE__ . '\execute_active_snippets', 1 ); +// add_action( 'plugins_loaded', __NAMESPACE__ . '\execute_active_snippets', 1 ); +add_action( 'plugins_loaded', __NAMESPACE__ . '\execute_active_snippets_from_flat_files', 1 ); diff --git a/src/php/snippet-ops.php b/src/php/snippet-ops.php index 2e3a9023..fd8c391e 100644 --- a/src/php/snippet-ops.php +++ b/src/php/snippet-ops.php @@ -314,7 +314,7 @@ function activate_snippet( int $id, ?bool $network = null ) { } update_shared_network_snippets( [ $snippet ] ); - do_action( 'code_snippets/activate_snippet', $snippet ); + do_action( 'code_snippets/activate_snippet', $snippet, $network ); clean_snippets_cache( $table_name ); return $snippet; } @@ -392,7 +392,7 @@ function deactivate_snippet( int $id, ?bool $network = null ): ?Snippet { $network = DB::validate_network_param( $network ); $table = code_snippets()->db->get_table_name( $network ); - // Set the snippet to active. + // Set the snippet to inactive. $result = $wpdb->update( $table, array( 'active' => '0' ), @@ -433,6 +433,8 @@ function delete_snippet( int $id, ?bool $network = null ): bool { $network = DB::validate_network_param( $network ); $table = code_snippets()->db->get_table_name( $network ); + $snippet = get_snippet( $id, $network ); + $result = $wpdb->delete( $table, array( 'id' => $id ), @@ -440,7 +442,7 @@ function delete_snippet( int $id, ?bool $network = null ): bool { ); if ( $result ) { - do_action( 'code_snippets/delete_snippet', $id, $network ); + do_action( 'code_snippets/delete_snippet', $snippet, $network ); clean_snippets_cache( $table ); code_snippets()->cloud_api->delete_snippet_from_transient_data( $id ); } @@ -670,6 +672,56 @@ function execute_active_snippets(): bool { return true; } +function execute_active_snippets_from_flat_files(): bool { + $tables = code_snippets()->db->get_active_tables(); + $scopes = array( 'global', 'single-use', is_admin() ? 'admin' : 'front-end' ); + + foreach ( $tables as $table ) { + $base_dir = WP_CONTENT_DIR . '/code-snippets/' . $table; + + if ( ! is_dir( $base_dir ) ) { + continue; + } + + $active_snippets_file_path = $base_dir . '/index.php'; + if ( ! is_file( $active_snippets_file_path ) ) { + continue; + } + + $active_snippets = require $active_snippets_file_path; + $sorted_snippets = sort_by_priority( $active_snippets ); + + foreach ( $sorted_snippets as $snippet_id => $snippet_data ) { + if ( ! in_array( $snippet_data['scope'], $scopes, true ) ) { + continue; + } + + $file = $base_dir . '/' . $snippet_id . '.php'; + execute_snippet_from_flat_file( $file, $snippet_id ); + } + } + + return true; +} + +function sort_by_priority( array $snippets ): array { + uasort( $snippets, function ( $a, $b ) { + return $a['priority'] <=> $b['priority']; + } ); + + return $snippets; +} + +function execute_snippet_from_flat_file( $file, int $id = 0, bool $force = false ) { + if ( ! is_file( $file ) || ( ! $force && defined( 'CODE_SNIPPETS_SAFE_MODE' ) && CODE_SNIPPETS_SAFE_MODE ) ) { + return false; + } + + require_once $file; + + do_action( 'code_snippets/after_execute_snippet_from_flat_file', $file, $id ); +} + /** * Retrieve a single snippets from the database using its cloud ID. * From 510e49e69da1ff6eb0c384469d97e09d46dcba84 Mon Sep 17 00:00:00 2001 From: Louis Wolmarans Date: Wed, 2 Jul 2025 09:15:12 +0200 Subject: [PATCH 003/318] wip --- src/php/class-snippet-files.php | 154 ++++++++++++++++---------------- 1 file changed, 76 insertions(+), 78 deletions(-) diff --git a/src/php/class-snippet-files.php b/src/php/class-snippet-files.php index f319b5ee..00e2f375 100644 --- a/src/php/class-snippet-files.php +++ b/src/php/class-snippet-files.php @@ -4,122 +4,120 @@ class Snippet_Files { - public function register_hooks() { - add_action( 'code_snippets/create_snippet', [ $this, 'handle_snippet' ], 10, 2 ); - add_action( 'code_snippets/update_snippet', [ $this, 'handle_snippet' ], 10, 2 ); - add_action( 'code_snippets/delete_snippet', [ $this, 'delete_snippet' ], 10, 2 ); - add_action( 'code_snippets/activate_snippet', [ $this, 'activate_snippet' ], 10, 2 ); - add_action( 'code_snippets/deactivate_snippet', [ $this, 'deactivate_snippet' ], 10, 2 ); - } + public function register_hooks() { + add_action( 'code_snippets/create_snippet', [ $this, 'handle_snippet' ], 10, 2 ); + add_action( 'code_snippets/update_snippet', [ $this, 'handle_snippet' ], 10, 2 ); + add_action( 'code_snippets/delete_snippet', [ $this, 'delete_snippet' ], 10, 2 ); + add_action( 'code_snippets/activate_snippet', [ $this, 'activate_snippet' ], 10, 2 ); + add_action( 'code_snippets/deactivate_snippet', [ $this, 'deactivate_snippet' ], 10, 2 ); + } - public function handle_snippet( $snippet, $table ) { - if ( 'php' !== $snippet->get_type() ) { - return; - } - $base_dir = WP_CONTENT_DIR . '/code-snippets/' . $table; + public function handle_snippet( $snippet, $table ) { + if ( 'php' !== $snippet->get_type() ) { + return; + } + $base_dir = WP_CONTENT_DIR . '/code-snippets/' . $table; - if ( ! is_dir( $base_dir ) ) { - wp_mkdir_p( $base_dir ); - } + if ( ! is_dir( $base_dir ) ) { + wp_mkdir_p( $base_dir ); + } - $file_path = trailingslashit( $base_dir ) . $snippet->id . '.php'; + $file_path = trailingslashit( $base_dir ) . $snippet->id . '.php'; - if ( $snippet->active ) { - $content = "code; + if ( $snippet->active ) { + $content = "code; - file_put_contents( $file_path, $content ); - } else { - @unlink( $file_path ); - } + file_put_contents( $file_path, $content ); + } else { + @unlink( $file_path ); + } - $index_file_path = trailingslashit( $base_dir ) . 'index.php'; + $index_file_path = trailingslashit( $base_dir ) . 'index.php'; - $active_snippets = is_file( $index_file_path ) ? require $index_file_path : []; + $active_snippets = is_file( $index_file_path ) ? require $index_file_path : []; - if ( $snippet->active ) { - $active_snippets[ $snippet->id ] = $snippet->get_fields(); - } else { - unset( $active_snippets[ $snippet->id ] ); - } - - $index_content = "active ) { + $active_snippets[ $snippet->id ] = $snippet->get_fields(); + } else { + unset( $active_snippets[ $snippet->id ] ); + } - public function delete_snippet( $snippet, $network ) { - if ( 'php' !== $snippet->get_type() ) { - return; - } + $index_content = "db->get_table_name( $network ); + file_put_contents( $index_file_path, $index_content ); + } - $base_dir = WP_CONTENT_DIR . '/code-snippets/' . $table; + public function delete_snippet( $snippet, $network ) { + if ( 'php' !== $snippet->get_type() ) { + return; + } - $file_path = trailingslashit( $base_dir ) . $snippet->id . '.php'; + $table = code_snippets()->db->get_table_name( $network ); - @unlink( $file_path ); + $base_dir = WP_CONTENT_DIR . '/code-snippets/' . $table; - $index_file_path = trailingslashit( $base_dir ) . 'index.php'; + $file_path = trailingslashit( $base_dir ) . $snippet->id . '.php'; - $active_snippets = is_file( $index_file_path ) ? require $index_file_path : []; + @unlink( $file_path ); - unset( $active_snippets[ $snippet_id ] ); + $index_file_path = trailingslashit( $base_dir ) . 'index.php'; - $index_content = "get_type() ) { - return; - } + $index_content = "db->get_table_name( $network ); + file_put_contents( $index_file_path, $index_content ); + } - $base_dir = WP_CONTENT_DIR . '/code-snippets/' . $table; + public function activate_snippet( $snippet, $network ) { + if ( 'php' !== $snippet->get_type() ) { + return; + } - $file_path = trailingslashit( $base_dir ) . $snippet->id . '.php'; + $table = code_snippets()->db->get_table_name( $network ); - $content = "code; + $base_dir = WP_CONTENT_DIR . '/code-snippets/' . $table; - file_put_contents( $file_path, $content ); + $file_path = trailingslashit( $base_dir ) . $snippet->id . '.php'; - $index_file_path = trailingslashit( $base_dir ) . 'index.php'; + $content = "code; - $active_snippets = is_file( $index_file_path ) ? require $index_file_path : []; + file_put_contents( $file_path, $content ); - $active_snippets[ $snippet->id ] = $snippet->get_fields(); + $index_file_path = trailingslashit( $base_dir ) . 'index.php'; - $index_content = "id ] = $snippet->get_fields(); - public function deactivate_snippet( $snippet_id, $network ) { - $snippet = get_snippet( $snippet_id, $network ); + $index_content = "get_type() ) { - return; - } + file_put_contents( $index_file_path, $index_content ); + } - $table = code_snippets()->db->get_table_name( $network ); + public function deactivate_snippet( $snippet_id, $network ) { + $snippet = get_snippet( $snippet_id, $network ); - $base_dir = WP_CONTENT_DIR . '/code-snippets/' . $table; + if ( 'php' !== $snippet->get_type() ) { + return; + } - $file_path = trailingslashit( $base_dir ) . $snippet->id . '.php'; + $base_dir = WP_CONTENT_DIR . '/code-snippets/' . $network; - @unlink( $file_path ); + $file_path = trailingslashit( $base_dir ) . $snippet_id . '.php'; - $index_file_path = trailingslashit( $base_dir ) . 'index.php'; + @unlink( $file_path ); - $active_snippets = is_file( $index_file_path ) ? require $index_file_path : []; + $index_file_path = trailingslashit( $base_dir ) . 'index.php'; - unset( $active_snippets[ $snippet_id ] ); + $active_snippets = is_file( $index_file_path ) ? require $index_file_path : []; - $index_content = " Date: Wed, 2 Jul 2025 09:29:04 +0200 Subject: [PATCH 004/318] wip --- src/php/class-snippet-files.php | 166 +++++++++++++++++++++----------- 1 file changed, 108 insertions(+), 58 deletions(-) diff --git a/src/php/class-snippet-files.php b/src/php/class-snippet-files.php index 00e2f375..d81572a9 100644 --- a/src/php/class-snippet-files.php +++ b/src/php/class-snippet-files.php @@ -4,6 +4,29 @@ class Snippet_Files { + /** + * Holds the WP_Filesystem instance. + * + * @var \WP_Filesystem_Base + */ + private $fs; + + public function __construct() { + $this->init_filesystem(); + } + + /** + * Initialize WP_Filesystem. + */ + private function init_filesystem() { + if ( ! function_exists( 'WP_Filesystem' ) ) { + require_once ABSPATH . 'wp-admin/includes/file.php'; + } + WP_Filesystem(); + global $wp_filesystem; + $this->fs = $wp_filesystem; + } + public function register_hooks() { add_action( 'code_snippets/create_snippet', [ $this, 'handle_snippet' ], 10, 2 ); add_action( 'code_snippets/update_snippet', [ $this, 'handle_snippet' ], 10, 2 ); @@ -16,35 +39,19 @@ public function handle_snippet( $snippet, $table ) { if ( 'php' !== $snippet->get_type() ) { return; } - $base_dir = WP_CONTENT_DIR . '/code-snippets/' . $table; - if ( ! is_dir( $base_dir ) ) { - wp_mkdir_p( $base_dir ); - } + $base_dir = $this->get_base_dir( $table ); + $this->maybe_create_directory( $base_dir ); - $file_path = trailingslashit( $base_dir ) . $snippet->id . '.php'; + $file_path = $this->get_snippet_file_path( $base_dir, $snippet->id ); if ( $snippet->active ) { - $content = "code; - - file_put_contents( $file_path, $content ); + $this->write_snippet_file( $file_path, $snippet->code ); } else { - @unlink( $file_path ); + $this->delete_file( $file_path ); } - $index_file_path = trailingslashit( $base_dir ) . 'index.php'; - - $active_snippets = is_file( $index_file_path ) ? require $index_file_path : []; - - if ( $snippet->active ) { - $active_snippets[ $snippet->id ] = $snippet->get_fields(); - } else { - unset( $active_snippets[ $snippet->id ] ); - } - - $index_content = "update_index_file( $base_dir, $snippet, $snippet->active ); } public function delete_snippet( $snippet, $network ) { @@ -53,22 +60,12 @@ public function delete_snippet( $snippet, $network ) { } $table = code_snippets()->db->get_table_name( $network ); + $base_dir = $this->get_base_dir( $table ); - $base_dir = WP_CONTENT_DIR . '/code-snippets/' . $table; - - $file_path = trailingslashit( $base_dir ) . $snippet->id . '.php'; - - @unlink( $file_path ); + $file_path = $this->get_snippet_file_path( $base_dir, $snippet->id ); + $this->delete_file( $file_path ); - $index_file_path = trailingslashit( $base_dir ) . 'index.php'; - - $active_snippets = is_file( $index_file_path ) ? require $index_file_path : []; - - unset( $active_snippets[ $snippet_id ] ); - - $index_content = "update_index_file( $base_dir, $snippet, false ); } public function activate_snippet( $snippet, $network ) { @@ -77,47 +74,100 @@ public function activate_snippet( $snippet, $network ) { } $table = code_snippets()->db->get_table_name( $network ); + $base_dir = $this->get_base_dir( $table ); - $base_dir = WP_CONTENT_DIR . '/code-snippets/' . $table; - - $file_path = trailingslashit( $base_dir ) . $snippet->id . '.php'; + $this->maybe_create_directory( $base_dir ); - $content = "code; + $file_path = $this->get_snippet_file_path( $base_dir, $snippet->id ); + $this->write_snippet_file( $file_path, $snippet->code ); - file_put_contents( $file_path, $content ); + $this->update_index_file( $base_dir, $snippet, true ); + } - $index_file_path = trailingslashit( $base_dir ) . 'index.php'; + public function deactivate_snippet( $snippet_id, $network ) { + $snippet = get_snippet( $snippet_id, $network ); - $active_snippets = is_file( $index_file_path ) ? require $index_file_path : []; + if ( ! $snippet || 'php' !== $snippet->get_type() ) { + return; + } - $active_snippets[ $snippet->id ] = $snippet->get_fields(); + $table = code_snippets()->db->get_table_name( $network ); + $base_dir = $this->get_base_dir( $table ); - $index_content = "get_snippet_file_path( $base_dir, $snippet_id ); + $this->delete_file( $file_path ); - file_put_contents( $index_file_path, $index_content ); + $this->update_index_file( $base_dir, $snippet, false ); } - public function deactivate_snippet( $snippet_id, $network ) { - $snippet = get_snippet( $snippet_id, $network ); + /** + * Returns the base directory path for a given table. + */ + private function get_base_dir( $table ) { + return WP_CONTENT_DIR . '/code-snippets/' . $table; + } - if ( 'php' !== $snippet->get_type() ) { - return; + /** + * Creates the directory if it does not exist. + */ + private function maybe_create_directory( $dir ) { + if ( ! $this->fs->is_dir( $dir ) ) { + $this->fs->mkdir( $dir, FS_CHMOD_DIR ); } + } - $base_dir = WP_CONTENT_DIR . '/code-snippets/' . $network; + /** + * Returns the path to the snippet PHP file. + */ + private function get_snippet_file_path( $base_dir, $snippet_id ) { + return trailingslashit( $base_dir ) . $snippet_id . '.php'; + } - $file_path = trailingslashit( $base_dir ) . $snippet_id . '.php'; + /** + * Writes the snippet code to a file, with the required header. + */ + private function write_snippet_file( $file_path, $code ) { + $content = "fs->put_contents( $file_path, $content, FS_CHMOD_FILE ); + } - @unlink( $file_path ); + /** + * Deletes a file if it exists. + */ + private function delete_file( $file_path ) { + if ( $this->fs->exists( $file_path ) ) { + $this->fs->delete( $file_path ); + } + } - $index_file_path = trailingslashit( $base_dir ) . 'index.php'; + /** + * Loads the index.php array by requiring it directly. + */ + private function load_index_file( $index_file_path ) { + return is_file( $index_file_path ) ? require $index_file_path : []; + } - $active_snippets = is_file( $index_file_path ) ? require $index_file_path : []; + /** + * Saves the index.php file via WP_Filesystem. + */ + private function save_index_file( $index_file_path, $active_snippets ) { + $index_content = "fs->put_contents( $index_file_path, $index_content, FS_CHMOD_FILE ); + } - unset( $active_snippets[ $snippet_id ] ); + /** + * Updates the index.php file by adding or removing a snippet. + */ + private function update_index_file( $base_dir, $snippet, $active ) { + $index_file_path = trailingslashit( $base_dir ) . 'index.php'; + $active_snippets = $this->load_index_file( $index_file_path ); - $index_content = "id ] = $snippet->get_fields(); + } else { + unset( $active_snippets[ $snippet->id ] ); + } - file_put_contents( $index_file_path, $index_content ); + $this->save_index_file( $index_file_path, $active_snippets ); } } From f65f5bc6f325392b0de9b75054d5afdb88a5ad57 Mon Sep 17 00:00:00 2001 From: Louis Wolmarans Date: Wed, 2 Jul 2025 09:43:56 +0200 Subject: [PATCH 005/318] wip --- src/php/class-plugin.php | 2 +- src/php/class-snippet-files.php | 28 ++++++++++++++-------------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/php/class-plugin.php b/src/php/class-plugin.php index 32a679db..fda65d6f 100644 --- a/src/php/class-plugin.php +++ b/src/php/class-plugin.php @@ -121,7 +121,7 @@ public function load_plugin() { // Snippet files. require_once $includes_path . '/class-snippet-files.php'; - ( new Snippet_Files() )->register_hooks(); + ( new Snippet_Files() )->init(); $this->active_snippets = new Active_Snippets(); $this->front_end = new Front_End(); diff --git a/src/php/class-snippet-files.php b/src/php/class-snippet-files.php index d81572a9..4cb1b9b5 100644 --- a/src/php/class-snippet-files.php +++ b/src/php/class-snippet-files.php @@ -11,20 +11,9 @@ class Snippet_Files { */ private $fs; - public function __construct() { - $this->init_filesystem(); - } - - /** - * Initialize WP_Filesystem. - */ - private function init_filesystem() { - if ( ! function_exists( 'WP_Filesystem' ) ) { - require_once ABSPATH . 'wp-admin/includes/file.php'; - } - WP_Filesystem(); - global $wp_filesystem; - $this->fs = $wp_filesystem; + public function init() { + $this->ensure_filesystem(); + $this->register_hooks(); } public function register_hooks() { @@ -35,6 +24,17 @@ public function register_hooks() { add_action( 'code_snippets/deactivate_snippet', [ $this, 'deactivate_snippet' ], 10, 2 ); } + private function ensure_filesystem() { + if ( ! $this->fs ) { + if ( ! function_exists( 'WP_Filesystem' ) ) { + require_once ABSPATH . 'wp-admin/includes/file.php'; + } + WP_Filesystem(); + global $wp_filesystem; + $this->fs = $wp_filesystem; + } + } + public function handle_snippet( $snippet, $table ) { if ( 'php' !== $snippet->get_type() ) { return; From c7f3d3cb49245f2fc1fd56e3b6b3204f8219f5d2 Mon Sep 17 00:00:00 2001 From: Louis Wolmarans Date: Wed, 2 Jul 2025 09:46:42 +0200 Subject: [PATCH 006/318] wip --- src/php/snippet-ops.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/php/snippet-ops.php b/src/php/snippet-ops.php index fd8c391e..5927ff7b 100644 --- a/src/php/snippet-ops.php +++ b/src/php/snippet-ops.php @@ -689,7 +689,7 @@ function execute_active_snippets_from_flat_files(): bool { } $active_snippets = require $active_snippets_file_path; - $sorted_snippets = sort_by_priority( $active_snippets ); + $sorted_snippets = cs_sort_snippets_by_priority( $active_snippets ); foreach ( $sorted_snippets as $snippet_id => $snippet_data ) { if ( ! in_array( $snippet_data['scope'], $scopes, true ) ) { @@ -704,7 +704,7 @@ function execute_active_snippets_from_flat_files(): bool { return true; } -function sort_by_priority( array $snippets ): array { +function cs_sort_snippets_by_priority( array $snippets ): array { uasort( $snippets, function ( $a, $b ) { return $a['priority'] <=> $b['priority']; } ); From 29a540c1b29a1d4c1017de3923445d511a1b5cb0 Mon Sep 17 00:00:00 2001 From: Louis Wolmarans Date: Wed, 2 Jul 2025 09:58:00 +0200 Subject: [PATCH 007/318] wip --- src/php/class-snippet-files.php | 24 ++++++++++++++---------- src/php/snippet-ops.php | 2 +- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/php/class-snippet-files.php b/src/php/class-snippet-files.php index 4cb1b9b5..bee41b59 100644 --- a/src/php/class-snippet-files.php +++ b/src/php/class-snippet-files.php @@ -36,11 +36,12 @@ private function ensure_filesystem() { } public function handle_snippet( $snippet, $table ) { - if ( 'php' !== $snippet->get_type() ) { + $snippet_type = $snippet->get_type(); + if ( 'php' !== $snippet_type ) { return; } - $base_dir = $this->get_base_dir( $table ); + $base_dir = $this->get_base_dir( $table, $snippet_type ); $this->maybe_create_directory( $base_dir ); $file_path = $this->get_snippet_file_path( $base_dir, $snippet->id ); @@ -55,12 +56,13 @@ public function handle_snippet( $snippet, $table ) { } public function delete_snippet( $snippet, $network ) { - if ( 'php' !== $snippet->get_type() ) { + $snippet_type = $snippet->get_type(); + if ( 'php' !== $snippet_type ) { return; } $table = code_snippets()->db->get_table_name( $network ); - $base_dir = $this->get_base_dir( $table ); + $base_dir = $this->get_base_dir( $table, $snippet_type ); $file_path = $this->get_snippet_file_path( $base_dir, $snippet->id ); $this->delete_file( $file_path ); @@ -69,12 +71,13 @@ public function delete_snippet( $snippet, $network ) { } public function activate_snippet( $snippet, $network ) { - if ( 'php' !== $snippet->get_type() ) { + $snippet_type = $snippet->get_type(); + if ( 'php' !== $snippet_type ) { return; } $table = code_snippets()->db->get_table_name( $network ); - $base_dir = $this->get_base_dir( $table ); + $base_dir = $this->get_base_dir( $table, $snippet_type ); $this->maybe_create_directory( $base_dir ); @@ -86,13 +89,14 @@ public function activate_snippet( $snippet, $network ) { public function deactivate_snippet( $snippet_id, $network ) { $snippet = get_snippet( $snippet_id, $network ); + $snippet_type = $snippet->get_type(); - if ( ! $snippet || 'php' !== $snippet->get_type() ) { + if ( ! $snippet || 'php' !== $snippet_type ) { return; } $table = code_snippets()->db->get_table_name( $network ); - $base_dir = $this->get_base_dir( $table ); + $base_dir = $this->get_base_dir( $table, $snippet_type ); $file_path = $this->get_snippet_file_path( $base_dir, $snippet_id ); $this->delete_file( $file_path ); @@ -103,8 +107,8 @@ public function deactivate_snippet( $snippet_id, $network ) { /** * Returns the base directory path for a given table. */ - private function get_base_dir( $table ) { - return WP_CONTENT_DIR . '/code-snippets/' . $table; + private function get_base_dir( $table, $snippet_type ) { + return WP_CONTENT_DIR . '/code-snippets/' . $table . '/' . $snippet_type; } /** diff --git a/src/php/snippet-ops.php b/src/php/snippet-ops.php index 5927ff7b..7cf791bd 100644 --- a/src/php/snippet-ops.php +++ b/src/php/snippet-ops.php @@ -677,7 +677,7 @@ function execute_active_snippets_from_flat_files(): bool { $scopes = array( 'global', 'single-use', is_admin() ? 'admin' : 'front-end' ); foreach ( $tables as $table ) { - $base_dir = WP_CONTENT_DIR . '/code-snippets/' . $table; + $base_dir = WP_CONTENT_DIR . '/code-snippets/' . $table . '/php'; if ( ! is_dir( $base_dir ) ) { continue; From c47c344cc004ddac7d961b7d59833040b7c81563 Mon Sep 17 00:00:00 2001 From: Louis Wolmarans Date: Wed, 2 Jul 2025 10:01:54 +0200 Subject: [PATCH 008/318] wip --- src/php/class-snippet-files.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/php/class-snippet-files.php b/src/php/class-snippet-files.php index bee41b59..bd313cde 100644 --- a/src/php/class-snippet-files.php +++ b/src/php/class-snippet-files.php @@ -36,7 +36,7 @@ private function ensure_filesystem() { } public function handle_snippet( $snippet, $table ) { - $snippet_type = $snippet->get_type(); + $snippet_type = $snippet->get_type(); if ( 'php' !== $snippet_type ) { return; } @@ -56,7 +56,7 @@ public function handle_snippet( $snippet, $table ) { } public function delete_snippet( $snippet, $network ) { - $snippet_type = $snippet->get_type(); + $snippet_type = $snippet->get_type(); if ( 'php' !== $snippet_type ) { return; } @@ -71,7 +71,7 @@ public function delete_snippet( $snippet, $network ) { } public function activate_snippet( $snippet, $network ) { - $snippet_type = $snippet->get_type(); + $snippet_type = $snippet->get_type(); if ( 'php' !== $snippet_type ) { return; } @@ -89,7 +89,7 @@ public function activate_snippet( $snippet, $network ) { public function deactivate_snippet( $snippet_id, $network ) { $snippet = get_snippet( $snippet_id, $network ); - $snippet_type = $snippet->get_type(); + $snippet_type = $snippet->get_type(); if ( ! $snippet || 'php' !== $snippet_type ) { return; From b66db45dde19a4092bac879ebfc90460acc8fcd9 Mon Sep 17 00:00:00 2001 From: Louis Wolmarans Date: Wed, 2 Jul 2025 10:46:05 +0200 Subject: [PATCH 009/318] wip --- src/php/class-snippet-files.php | 23 +++++++++++++++-------- src/php/front-end/class-front-end.php | 27 +++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/src/php/class-snippet-files.php b/src/php/class-snippet-files.php index bd313cde..6fdadfcb 100644 --- a/src/php/class-snippet-files.php +++ b/src/php/class-snippet-files.php @@ -37,7 +37,7 @@ private function ensure_filesystem() { public function handle_snippet( $snippet, $table ) { $snippet_type = $snippet->get_type(); - if ( 'php' !== $snippet_type ) { + if ( 'php' !== $snippet_type && 'html' !== $snippet_type ) { return; } @@ -47,7 +47,7 @@ public function handle_snippet( $snippet, $table ) { $file_path = $this->get_snippet_file_path( $base_dir, $snippet->id ); if ( $snippet->active ) { - $this->write_snippet_file( $file_path, $snippet->code ); + $this->write_snippet_file( $file_path, $snippet->code, $snippet_type ); } else { $this->delete_file( $file_path ); } @@ -57,7 +57,7 @@ public function handle_snippet( $snippet, $table ) { public function delete_snippet( $snippet, $network ) { $snippet_type = $snippet->get_type(); - if ( 'php' !== $snippet_type ) { + if ( 'php' !== $snippet_type && 'html' !== $snippet_type ) { return; } @@ -72,7 +72,7 @@ public function delete_snippet( $snippet, $network ) { public function activate_snippet( $snippet, $network ) { $snippet_type = $snippet->get_type(); - if ( 'php' !== $snippet_type ) { + if ( 'php' !== $snippet_type && 'html' !== $snippet_type ) { return; } @@ -82,7 +82,7 @@ public function activate_snippet( $snippet, $network ) { $this->maybe_create_directory( $base_dir ); $file_path = $this->get_snippet_file_path( $base_dir, $snippet->id ); - $this->write_snippet_file( $file_path, $snippet->code ); + $this->write_snippet_file( $file_path, $snippet->code, $snippet_type ); $this->update_index_file( $base_dir, $snippet, true ); } @@ -91,7 +91,7 @@ public function deactivate_snippet( $snippet_id, $network ) { $snippet = get_snippet( $snippet_id, $network ); $snippet_type = $snippet->get_type(); - if ( ! $snippet || 'php' !== $snippet_type ) { + if ( 'php' !== $snippet_type && 'html' !== $snippet_type ) { return; } @@ -130,8 +130,15 @@ private function get_snippet_file_path( $base_dir, $snippet_id ) { /** * Writes the snippet code to a file, with the required header. */ - private function write_snippet_file( $file_path, $code ) { - $content = "\n\n"; + } + + $content .= $code; + $this->fs->put_contents( $file_path, $content, FS_CHMOD_FILE ); } diff --git a/src/php/front-end/class-front-end.php b/src/php/front-end/class-front-end.php index 8d76ae8a..60429b35 100644 --- a/src/php/front-end/class-front-end.php +++ b/src/php/front-end/class-front-end.php @@ -245,6 +245,16 @@ protected function evaluate_shortcode_content( Snippet $snippet, array $atts ): return $snippet->code; } + $network = DB::validate_network_param( $snippet->network ); + $table_name = code_snippets()->db->get_table_name( $network ); + $filepath = WP_CONTENT_DIR . '/code-snippets/' . $table_name . '/html/' . $snippet->id . '.php'; + + return file_exists( $filepath ) + ? $this->evaluate_shortcode_from_flat_file( $filepath, $atts ) + : $this->evaluate_shortcode_from_db( $snippet, $atts ); + } + + private function evaluate_shortcode_from_db( Snippet $snippet, array $atts ): string { /** * Avoiding extract is typically recommended, however in this situation we want to make it easy for snippet * authors to use custom attributes. @@ -259,6 +269,23 @@ protected function evaluate_shortcode_content( Snippet $snippet, array $atts ): return ob_get_clean(); } + private function evaluate_shortcode_from_flat_file( $filepath, array $atts ): string { + ob_start(); + + ( function( $atts ) use ( $filepath ) { + /** + * Avoiding extract is typically recommended, however in this situation we want to make it easy for snippet + * authors to use custom attributes. + * + * @phpcs:disable WordPress.PHP.DontExtract.extract_extract + */ + extract( $atts ); + require_once $filepath; + } )( $atts ); + + return ob_get_clean(); + } + /** * Render the value of a content shortcode * From 55561ca4d233bcfe94dd66f10de11adc057c984e Mon Sep 17 00:00:00 2001 From: Louis Wolmarans Date: Wed, 2 Jul 2025 10:50:19 +0200 Subject: [PATCH 010/318] wip --- src/php/class-snippet-files.php | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/php/class-snippet-files.php b/src/php/class-snippet-files.php index 6fdadfcb..92d18ab4 100644 --- a/src/php/class-snippet-files.php +++ b/src/php/class-snippet-files.php @@ -52,7 +52,7 @@ public function handle_snippet( $snippet, $table ) { $this->delete_file( $file_path ); } - $this->update_index_file( $base_dir, $snippet, $snippet->active ); + $this->update_config_file( $base_dir, $snippet, $snippet->active ); } public function delete_snippet( $snippet, $network ) { @@ -67,7 +67,7 @@ public function delete_snippet( $snippet, $network ) { $file_path = $this->get_snippet_file_path( $base_dir, $snippet->id ); $this->delete_file( $file_path ); - $this->update_index_file( $base_dir, $snippet, false ); + $this->update_config_file( $base_dir, $snippet, false ); } public function activate_snippet( $snippet, $network ) { @@ -84,7 +84,7 @@ public function activate_snippet( $snippet, $network ) { $file_path = $this->get_snippet_file_path( $base_dir, $snippet->id ); $this->write_snippet_file( $file_path, $snippet->code, $snippet_type ); - $this->update_index_file( $base_dir, $snippet, true ); + $this->update_config_file( $base_dir, $snippet, true ); } public function deactivate_snippet( $snippet_id, $network ) { @@ -101,7 +101,7 @@ public function deactivate_snippet( $snippet_id, $network ) { $file_path = $this->get_snippet_file_path( $base_dir, $snippet_id ); $this->delete_file( $file_path ); - $this->update_index_file( $base_dir, $snippet, false ); + $this->update_config_file( $base_dir, $snippet, false ); } /** @@ -133,9 +133,9 @@ private function get_snippet_file_path( $base_dir, $snippet_id ) { private function write_snippet_file( $file_path, $code, $snippet_type ) { $content = "\n\n"; - } + if ( 'html' === $snippet_type ) { + $content .= "?>\n\n"; + } $content .= $code; @@ -154,24 +154,24 @@ private function delete_file( $file_path ) { /** * Loads the index.php array by requiring it directly. */ - private function load_index_file( $index_file_path ) { - return is_file( $index_file_path ) ? require $index_file_path : []; + private function load_config_file( $config_file_path ) { + return is_file( $config_file_path ) ? require $config_file_path : []; } /** * Saves the index.php file via WP_Filesystem. */ - private function save_index_file( $index_file_path, $active_snippets ) { + private function save_config_file( $config_file_path, $active_snippets ) { $index_content = "fs->put_contents( $index_file_path, $index_content, FS_CHMOD_FILE ); + $this->fs->put_contents( $config_file_path, $index_content, FS_CHMOD_FILE ); } /** * Updates the index.php file by adding or removing a snippet. */ - private function update_index_file( $base_dir, $snippet, $active ) { - $index_file_path = trailingslashit( $base_dir ) . 'index.php'; - $active_snippets = $this->load_index_file( $index_file_path ); + private function update_config_file( $base_dir, $snippet, $active ) { + $config_file_path = trailingslashit( $base_dir ) . 'index.php'; + $active_snippets = $this->load_config_file( $config_file_path ); if ( $active ) { $active_snippets[ $snippet->id ] = $snippet->get_fields(); @@ -179,6 +179,6 @@ private function update_index_file( $base_dir, $snippet, $active ) { unset( $active_snippets[ $snippet->id ] ); } - $this->save_index_file( $index_file_path, $active_snippets ); + $this->save_config_file( $config_file_path, $active_snippets ); } } From f2720ace011c9edd529f4e63cea633d699039910 Mon Sep 17 00:00:00 2001 From: Louis Wolmarans Date: Wed, 2 Jul 2025 12:39:17 +0200 Subject: [PATCH 011/318] wip --- src/php/class-snippet-files.php | 51 +++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/src/php/class-snippet-files.php b/src/php/class-snippet-files.php index 92d18ab4..1040df23 100644 --- a/src/php/class-snippet-files.php +++ b/src/php/class-snippet-files.php @@ -11,17 +11,19 @@ class Snippet_Files { */ private $fs; + const TYPES_TO_HANDLE = [ 'php', 'html' ]; + public function init() { $this->ensure_filesystem(); $this->register_hooks(); } public function register_hooks() { - add_action( 'code_snippets/create_snippet', [ $this, 'handle_snippet' ], 10, 2 ); - add_action( 'code_snippets/update_snippet', [ $this, 'handle_snippet' ], 10, 2 ); - add_action( 'code_snippets/delete_snippet', [ $this, 'delete_snippet' ], 10, 2 ); - add_action( 'code_snippets/activate_snippet', [ $this, 'activate_snippet' ], 10, 2 ); - add_action( 'code_snippets/deactivate_snippet', [ $this, 'deactivate_snippet' ], 10, 2 ); + add_action( 'code_snippets/create_snippet', array( $this, 'handle_snippet' ), 10, 2 ); + add_action( 'code_snippets/update_snippet', array( $this, 'handle_snippet' ), 10, 2 ); + add_action( 'code_snippets/delete_snippet', array( $this, 'delete_snippet' ), 10, 2 ); + add_action( 'code_snippets/activate_snippet', array( $this, 'activate_snippet' ), 10, 2 ); + add_action( 'code_snippets/deactivate_snippet', array( $this, 'deactivate_snippet' ), 10, 2 ); } private function ensure_filesystem() { @@ -29,15 +31,22 @@ private function ensure_filesystem() { if ( ! function_exists( 'WP_Filesystem' ) ) { require_once ABSPATH . 'wp-admin/includes/file.php'; } + WP_Filesystem(); + global $wp_filesystem; + $this->fs = $wp_filesystem; } } - public function handle_snippet( $snippet, $table ) { + private function should_handle_snippet( string $snippet_type ) { + return in_array( $snippet_type, self::TYPES_TO_HANDLE, true ); + } + + public function handle_snippet( Snippet $snippet, string $table ) { $snippet_type = $snippet->get_type(); - if ( 'php' !== $snippet_type && 'html' !== $snippet_type ) { + if ( ! $this->should_handle_snippet( $snippet_type ) ) { return; } @@ -55,9 +64,9 @@ public function handle_snippet( $snippet, $table ) { $this->update_config_file( $base_dir, $snippet, $snippet->active ); } - public function delete_snippet( $snippet, $network ) { + public function delete_snippet( Snippet $snippet, bool $network ) { $snippet_type = $snippet->get_type(); - if ( 'php' !== $snippet_type && 'html' !== $snippet_type ) { + if ( ! $this->should_handle_snippet( $snippet_type ) ) { return; } @@ -70,9 +79,9 @@ public function delete_snippet( $snippet, $network ) { $this->update_config_file( $base_dir, $snippet, false ); } - public function activate_snippet( $snippet, $network ) { + public function activate_snippet( Snippet $snippet, bool $network ) { $snippet_type = $snippet->get_type(); - if ( 'php' !== $snippet_type && 'html' !== $snippet_type ) { + if ( ! $this->should_handle_snippet( $snippet_type ) ) { return; } @@ -87,11 +96,11 @@ public function activate_snippet( $snippet, $network ) { $this->update_config_file( $base_dir, $snippet, true ); } - public function deactivate_snippet( $snippet_id, $network ) { + public function deactivate_snippet( int $snippet_id, bool $network ) { $snippet = get_snippet( $snippet_id, $network ); $snippet_type = $snippet->get_type(); - if ( 'php' !== $snippet_type && 'html' !== $snippet_type ) { + if ( ! $this->should_handle_snippet( $snippet_type ) ) { return; } @@ -107,14 +116,14 @@ public function deactivate_snippet( $snippet_id, $network ) { /** * Returns the base directory path for a given table. */ - private function get_base_dir( $table, $snippet_type ) { + private function get_base_dir( string $table, string $snippet_type ) { return WP_CONTENT_DIR . '/code-snippets/' . $table . '/' . $snippet_type; } /** * Creates the directory if it does not exist. */ - private function maybe_create_directory( $dir ) { + private function maybe_create_directory( string $dir ) { if ( ! $this->fs->is_dir( $dir ) ) { $this->fs->mkdir( $dir, FS_CHMOD_DIR ); } @@ -123,14 +132,14 @@ private function maybe_create_directory( $dir ) { /** * Returns the path to the snippet PHP file. */ - private function get_snippet_file_path( $base_dir, $snippet_id ) { + private function get_snippet_file_path( string $base_dir, int $snippet_id ) { return trailingslashit( $base_dir ) . $snippet_id . '.php'; } /** * Writes the snippet code to a file, with the required header. */ - private function write_snippet_file( $file_path, $code, $snippet_type ) { + private function write_snippet_file( string $file_path, string $code, string $snippet_type ) { $content = "fs->exists( $file_path ) ) { $this->fs->delete( $file_path ); } @@ -154,14 +163,14 @@ private function delete_file( $file_path ) { /** * Loads the index.php array by requiring it directly. */ - private function load_config_file( $config_file_path ) { + private function load_config_file( string $config_file_path ) { return is_file( $config_file_path ) ? require $config_file_path : []; } /** * Saves the index.php file via WP_Filesystem. */ - private function save_config_file( $config_file_path, $active_snippets ) { + private function save_config_file( string $config_file_path, array $active_snippets ) { $index_content = "fs->put_contents( $config_file_path, $index_content, FS_CHMOD_FILE ); } @@ -169,7 +178,7 @@ private function save_config_file( $config_file_path, $active_snippets ) { /** * Updates the index.php file by adding or removing a snippet. */ - private function update_config_file( $base_dir, $snippet, $active ) { + private function update_config_file( string $base_dir, Snippet $snippet, bool $active ) { $config_file_path = trailingslashit( $base_dir ) . 'index.php'; $active_snippets = $this->load_config_file( $config_file_path ); From 6477f86889b33c2b2756b5570c6aa11ac283ce9e Mon Sep 17 00:00:00 2001 From: Louis Wolmarans Date: Wed, 2 Jul 2025 13:01:40 +0200 Subject: [PATCH 012/318] wip --- src/php/snippet-ops.php | 68 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 62 insertions(+), 6 deletions(-) diff --git a/src/php/snippet-ops.php b/src/php/snippet-ops.php index 7cf791bd..d6200091 100644 --- a/src/php/snippet-ops.php +++ b/src/php/snippet-ops.php @@ -659,6 +659,9 @@ function execute_active_snippets(): bool { array( '%d' ) ); clean_snippets_cache( $table_name ); + + $network = $table_name === $db->ms_table; + do_action( 'code_snippets/deactivate_snippet', $snippet_id, $network ); } } @@ -672,12 +675,38 @@ function execute_active_snippets(): bool { return true; } +/** + * Execute the active snippets from the flat files. + * Read-write-execute operation. + * + * @return bool true on success, false on failure. + */ function execute_active_snippets_from_flat_files(): bool { - $tables = code_snippets()->db->get_active_tables(); + $db = code_snippets()->db; + $tables = $db->get_active_tables(); $scopes = array( 'global', 'single-use', is_admin() ? 'admin' : 'front-end' ); - foreach ( $tables as $table ) { - $base_dir = WP_CONTENT_DIR . '/code-snippets/' . $table . '/php'; + // Detect if a snippet is currently being edited, and if so, spare it from execution. + $edit_id = 0; + $edit_table = $db->table; + + if ( wp_is_json_request() && ! empty( $_SERVER['REQUEST_URI'] ) ) { + $url = wp_parse_url( esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) ); + + if ( isset( $url['path'] ) && false !== strpos( $url['path'], Snippets_REST_Controller::get_prefixed_base_route() ) ) { + $path_parts = explode( '/', $url['path'] ); + $edit_id = intval( end( $path_parts ) ); + + if ( ! empty( $url['query'] ) ) { + wp_parse_str( $url['query'], $path_params ); + $edit_table = isset( $path_params['network'] ) && rest_sanitize_boolean( $path_params['network'] ) ? + $db->ms_table : $db->table; + } + } + } + + foreach ( $tables as $table_name ) { + $base_dir = WP_CONTENT_DIR . '/code-snippets/' . $table_name . '/php'; if ( ! is_dir( $base_dir ) ) { continue; @@ -691,13 +720,40 @@ function execute_active_snippets_from_flat_files(): bool { $active_snippets = require $active_snippets_file_path; $sorted_snippets = cs_sort_snippets_by_priority( $active_snippets ); - foreach ( $sorted_snippets as $snippet_id => $snippet_data ) { - if ( ! in_array( $snippet_data['scope'], $scopes, true ) ) { + foreach ( $sorted_snippets as $snippet_id => $snippet ) { + if ( ! in_array( $snippet['scope'], $scopes, true ) ) { continue; } + // If the snippet is a single-use snippet, deactivate it before execution to ensure that the process always happens. + if ( 'single-use' === $snippet['scope'] ) { + $active_shared_ids = get_option( 'active_shared_network_snippets', array() ); + + if ( $table_name === $db->ms_table && is_array( $active_shared_ids ) && in_array( $snippet_id, $active_shared_ids, true ) ) { + unset( $active_shared_ids[ array_search( $snippet_id, $active_shared_ids, true ) ] ); + $active_shared_ids = array_values( $active_shared_ids ); + update_option( 'active_shared_network_snippets', $active_shared_ids ); + clean_active_snippets_cache( $table_name ); + } else { + $wpdb->update( + $table_name, + array( 'active' => '0' ), + array( 'id' => $snippet_id ), + array( '%d' ), + array( '%d' ) + ); + clean_snippets_cache( $table_name ); + + $network = $table_name === $db->ms_table; + do_action( 'code_snippets/deactivate_snippet', $snippet_id, $network ); + } + } + $file = $base_dir . '/' . $snippet_id . '.php'; - execute_snippet_from_flat_file( $file, $snippet_id ); + if ( apply_filters( 'code_snippets/allow_execute_snippet', true, $snippet_id, $table_name ) && + ! ( $edit_id === $snippet_id && $table_name === $edit_table ) ) { + execute_snippet_from_flat_file( $file, $snippet_id ); + } } } From 0eda7c915b2a0e4747954afd34df5ebc77fef6e0 Mon Sep 17 00:00:00 2001 From: Louis Wolmarans Date: Wed, 2 Jul 2025 13:09:46 +0200 Subject: [PATCH 013/318] wip --- src/php/class-snippet-files.php | 10 +++++----- src/php/snippet-ops.php | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/php/class-snippet-files.php b/src/php/class-snippet-files.php index 1040df23..cf3b7302 100644 --- a/src/php/class-snippet-files.php +++ b/src/php/class-snippet-files.php @@ -50,7 +50,7 @@ public function handle_snippet( Snippet $snippet, string $table ) { return; } - $base_dir = $this->get_base_dir( $table, $snippet_type ); + $base_dir = self::get_base_dir( $table, $snippet_type ); $this->maybe_create_directory( $base_dir ); $file_path = $this->get_snippet_file_path( $base_dir, $snippet->id ); @@ -71,7 +71,7 @@ public function delete_snippet( Snippet $snippet, bool $network ) { } $table = code_snippets()->db->get_table_name( $network ); - $base_dir = $this->get_base_dir( $table, $snippet_type ); + $base_dir = self::get_base_dir( $table, $snippet_type ); $file_path = $this->get_snippet_file_path( $base_dir, $snippet->id ); $this->delete_file( $file_path ); @@ -86,7 +86,7 @@ public function activate_snippet( Snippet $snippet, bool $network ) { } $table = code_snippets()->db->get_table_name( $network ); - $base_dir = $this->get_base_dir( $table, $snippet_type ); + $base_dir = self::get_base_dir( $table, $snippet_type ); $this->maybe_create_directory( $base_dir ); @@ -105,7 +105,7 @@ public function deactivate_snippet( int $snippet_id, bool $network ) { } $table = code_snippets()->db->get_table_name( $network ); - $base_dir = $this->get_base_dir( $table, $snippet_type ); + $base_dir = self::get_base_dir( $table, $snippet_type ); $file_path = $this->get_snippet_file_path( $base_dir, $snippet_id ); $this->delete_file( $file_path ); @@ -116,7 +116,7 @@ public function deactivate_snippet( int $snippet_id, bool $network ) { /** * Returns the base directory path for a given table. */ - private function get_base_dir( string $table, string $snippet_type ) { + public static function get_base_dir( string $table, string $snippet_type ) { return WP_CONTENT_DIR . '/code-snippets/' . $table . '/' . $snippet_type; } diff --git a/src/php/snippet-ops.php b/src/php/snippet-ops.php index d6200091..dc96a6df 100644 --- a/src/php/snippet-ops.php +++ b/src/php/snippet-ops.php @@ -706,7 +706,7 @@ function execute_active_snippets_from_flat_files(): bool { } foreach ( $tables as $table_name ) { - $base_dir = WP_CONTENT_DIR . '/code-snippets/' . $table_name . '/php'; + $base_dir = Snippet_Files::get_base_dir( $table_name, 'php' ); if ( ! is_dir( $base_dir ) ) { continue; From 759aabdedf2460adad77b95053e3a9cf334c4a1a Mon Sep 17 00:00:00 2001 From: Louis Wolmarans Date: Wed, 2 Jul 2025 13:35:15 +0200 Subject: [PATCH 014/318] wip --- src/php/snippet-ops.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/php/snippet-ops.php b/src/php/snippet-ops.php index dc96a6df..91326aec 100644 --- a/src/php/snippet-ops.php +++ b/src/php/snippet-ops.php @@ -682,6 +682,12 @@ function execute_active_snippets(): bool { * @return bool true on success, false on failure. */ function execute_active_snippets_from_flat_files(): bool { + // Bail early if safe mode is active. + if ( ( defined( 'CODE_SNIPPETS_SAFE_MODE' ) && CODE_SNIPPETS_SAFE_MODE ) || + ! apply_filters( 'code_snippets/execute_snippets', true ) ) { + return false; + } + $db = code_snippets()->db; $tables = $db->get_active_tables(); $scopes = array( 'global', 'single-use', is_admin() ? 'admin' : 'front-end' ); From 18bcfa399954a92cbe8cda4df9999183944c0707 Mon Sep 17 00:00:00 2001 From: Louis Wolmarans Date: Wed, 2 Jul 2025 14:40:19 +0200 Subject: [PATCH 015/318] wip --- src/php/snippet-ops.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/php/snippet-ops.php b/src/php/snippet-ops.php index 91326aec..34d2c787 100644 --- a/src/php/snippet-ops.php +++ b/src/php/snippet-ops.php @@ -682,6 +682,8 @@ function execute_active_snippets(): bool { * @return bool true on success, false on failure. */ function execute_active_snippets_from_flat_files(): bool { + global $wpdb; + // Bail early if safe mode is active. if ( ( defined( 'CODE_SNIPPETS_SAFE_MODE' ) && CODE_SNIPPETS_SAFE_MODE ) || ! apply_filters( 'code_snippets/execute_snippets', true ) ) { From dc91357dc608e501df27926fe35e385a1c37bc76 Mon Sep 17 00:00:00 2001 From: Louis Wolmarans Date: Wed, 2 Jul 2025 21:46:20 +0200 Subject: [PATCH 016/318] wip --- src/php/class-snippet-files.php | 37 +++++++++++++-------------------- src/php/snippet-ops.php | 4 ++++ 2 files changed, 19 insertions(+), 22 deletions(-) diff --git a/src/php/class-snippet-files.php b/src/php/class-snippet-files.php index cf3b7302..3aa97f7a 100644 --- a/src/php/class-snippet-files.php +++ b/src/php/class-snippet-files.php @@ -19,11 +19,11 @@ public function init() { } public function register_hooks() { - add_action( 'code_snippets/create_snippet', array( $this, 'handle_snippet' ), 10, 2 ); - add_action( 'code_snippets/update_snippet', array( $this, 'handle_snippet' ), 10, 2 ); - add_action( 'code_snippets/delete_snippet', array( $this, 'delete_snippet' ), 10, 2 ); - add_action( 'code_snippets/activate_snippet', array( $this, 'activate_snippet' ), 10, 2 ); - add_action( 'code_snippets/deactivate_snippet', array( $this, 'deactivate_snippet' ), 10, 2 ); + add_action( 'code_snippets/create_snippet', [ $this, 'handle_snippet' ], 10, 2 ); + add_action( 'code_snippets/update_snippet', [ $this, 'handle_snippet' ], 10, 2 ); + add_action( 'code_snippets/delete_snippet', [ $this, 'delete_snippet' ], 10, 2 ); + add_action( 'code_snippets/activate_snippet', [ $this, 'activate_snippet' ], 10, 2 ); + add_action( 'code_snippets/deactivate_snippet', [ $this, 'deactivate_snippet' ], 10, 2 ); } private function ensure_filesystem() { @@ -55,13 +55,9 @@ public function handle_snippet( Snippet $snippet, string $table ) { $file_path = $this->get_snippet_file_path( $base_dir, $snippet->id ); - if ( $snippet->active ) { - $this->write_snippet_file( $file_path, $snippet->code, $snippet_type ); - } else { - $this->delete_file( $file_path ); - } + $this->write_snippet_file( $file_path, $snippet->code, $snippet_type ); - $this->update_config_file( $base_dir, $snippet, $snippet->active ); + $this->update_config_file( $base_dir, $snippet ); } public function delete_snippet( Snippet $snippet, bool $network ) { @@ -76,7 +72,7 @@ public function delete_snippet( Snippet $snippet, bool $network ) { $file_path = $this->get_snippet_file_path( $base_dir, $snippet->id ); $this->delete_file( $file_path ); - $this->update_config_file( $base_dir, $snippet, false ); + $this->update_config_file( $base_dir, $snippet, true ); } public function activate_snippet( Snippet $snippet, bool $network ) { @@ -93,7 +89,7 @@ public function activate_snippet( Snippet $snippet, bool $network ) { $file_path = $this->get_snippet_file_path( $base_dir, $snippet->id ); $this->write_snippet_file( $file_path, $snippet->code, $snippet_type ); - $this->update_config_file( $base_dir, $snippet, true ); + $this->update_config_file( $base_dir, $snippet ); } public function deactivate_snippet( int $snippet_id, bool $network ) { @@ -107,10 +103,7 @@ public function deactivate_snippet( int $snippet_id, bool $network ) { $table = code_snippets()->db->get_table_name( $network ); $base_dir = self::get_base_dir( $table, $snippet_type ); - $file_path = $this->get_snippet_file_path( $base_dir, $snippet_id ); - $this->delete_file( $file_path ); - - $this->update_config_file( $base_dir, $snippet, false ); + $this->update_config_file( $base_dir, $snippet ); } /** @@ -176,16 +169,16 @@ private function save_config_file( string $config_file_path, array $active_snipp } /** - * Updates the index.php file by adding or removing a snippet. + * Updates the index.php file with snippet config. */ - private function update_config_file( string $base_dir, Snippet $snippet, bool $active ) { + private function update_config_file( string $base_dir, Snippet $snippet, bool $remove = false ) { $config_file_path = trailingslashit( $base_dir ) . 'index.php'; $active_snippets = $this->load_config_file( $config_file_path ); - if ( $active ) { - $active_snippets[ $snippet->id ] = $snippet->get_fields(); - } else { + if ( $remove ) { unset( $active_snippets[ $snippet->id ] ); + } else { + $active_snippets[ $snippet->id ] = $snippet->get_fields(); } $this->save_config_file( $config_file_path, $active_snippets ); diff --git a/src/php/snippet-ops.php b/src/php/snippet-ops.php index 34d2c787..39565c59 100644 --- a/src/php/snippet-ops.php +++ b/src/php/snippet-ops.php @@ -733,6 +733,10 @@ function execute_active_snippets_from_flat_files(): bool { continue; } + if ( ! $snippet['active'] ) { + continue; + } + // If the snippet is a single-use snippet, deactivate it before execution to ensure that the process always happens. if ( 'single-use' === $snippet['scope'] ) { $active_shared_ids = get_option( 'active_shared_network_snippets', array() ); From 1d99420c70a43658f380f89c832540086f997161 Mon Sep 17 00:00:00 2001 From: Louis Wolmarans Date: Wed, 2 Jul 2025 22:28:45 +0200 Subject: [PATCH 017/318] wip --- src/php/class-snippet-files.php | 37 ++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/src/php/class-snippet-files.php b/src/php/class-snippet-files.php index 3aa97f7a..3219540e 100644 --- a/src/php/class-snippet-files.php +++ b/src/php/class-snippet-files.php @@ -24,6 +24,7 @@ public function register_hooks() { add_action( 'code_snippets/delete_snippet', [ $this, 'delete_snippet' ], 10, 2 ); add_action( 'code_snippets/activate_snippet', [ $this, 'activate_snippet' ], 10, 2 ); add_action( 'code_snippets/deactivate_snippet', [ $this, 'deactivate_snippet' ], 10, 2 ); + add_action( 'updated_option', [ $this, 'sync_active_shared_network_snippets' ], 10, 3 ); } private function ensure_filesystem() { @@ -107,10 +108,20 @@ public function deactivate_snippet( int $snippet_id, bool $network ) { } /** - * Returns the base directory path for a given table. + * Returns the base directory path for a given context. */ - public static function get_base_dir( string $table, string $snippet_type ) { - return WP_CONTENT_DIR . '/code-snippets/' . $table . '/' . $snippet_type; + public static function get_base_dir( string $table = '', string $snippet_type = '' ) { + $base_dir = WP_CONTENT_DIR . '/code-snippets'; + + if ( ! empty( $table ) ) { + $base_dir .= '/' . $table; + } + + if ( ! empty( $snippet_type ) ) { + $base_dir .= '/' . $snippet_type; + } + + return $base_dir; } /** @@ -164,8 +175,8 @@ private function load_config_file( string $config_file_path ) { * Saves the index.php file via WP_Filesystem. */ private function save_config_file( string $config_file_path, array $active_snippets ) { - $index_content = "fs->put_contents( $config_file_path, $index_content, FS_CHMOD_FILE ); + $file_content = "fs->put_contents( $config_file_path, $file_content, FS_CHMOD_FILE ); } /** @@ -183,4 +194,20 @@ private function update_config_file( string $base_dir, Snippet $snippet, bool $r $this->save_config_file( $config_file_path, $active_snippets ); } + + public function sync_active_shared_network_snippets( $option, $old_value, $value ) { + if ( 'active_shared_network_snippets' !== $option ) { + return; + } + + $table = code_snippets()->db->get_table_name(); + $base_dir = self::get_base_dir( $table ); + + $this->maybe_create_directory( $base_dir ); + + $file_path = trailingslashit( $base_dir ) . 'active-shared-network-snippets.php'; + $file_content = "fs->put_contents( $file_path, $file_content, FS_CHMOD_FILE ); + } } From 918e7697737b95c49be29342762cbc517917c455 Mon Sep 17 00:00:00 2001 From: Louis Wolmarans Date: Wed, 2 Jul 2025 23:25:38 +0200 Subject: [PATCH 018/318] wip --- src/php/class-snippet-files.php | 52 ++++++++++++++++++++++++++++++--- src/php/snippet-ops.php | 37 +++++------------------ 2 files changed, 56 insertions(+), 33 deletions(-) diff --git a/src/php/class-snippet-files.php b/src/php/class-snippet-files.php index 3219540e..4fc0bab0 100644 --- a/src/php/class-snippet-files.php +++ b/src/php/class-snippet-files.php @@ -77,6 +77,8 @@ public function delete_snippet( Snippet $snippet, bool $network ) { } public function activate_snippet( Snippet $snippet, bool $network ) { + $snippet = get_snippet( $snippet->id, $network ); + $snippet_type = $snippet->get_type(); if ( ! $this->should_handle_snippet( $snippet_type ) ) { return; @@ -182,14 +184,14 @@ private function save_config_file( string $config_file_path, array $active_snipp /** * Updates the index.php file with snippet config. */ - private function update_config_file( string $base_dir, Snippet $snippet, bool $remove = false ) { + private function update_config_file( string $base_dir, Snippet $snippet, ?bool $remove = false ) { $config_file_path = trailingslashit( $base_dir ) . 'index.php'; $active_snippets = $this->load_config_file( $config_file_path ); - if ( $remove ) { - unset( $active_snippets[ $snippet->id ] ); - } else { + if ( ! $remove ) { $active_snippets[ $snippet->id ] = $snippet->get_fields(); + } else { + unset( $active_snippets[ $snippet->id ] ); } $this->save_config_file( $config_file_path, $active_snippets ); @@ -210,4 +212,46 @@ public function sync_active_shared_network_snippets( $option, $old_value, $value $this->fs->put_contents( $file_path, $file_content, FS_CHMOD_FILE ); } + + public static function get_active_snippets_from_flat_files() { + $snippets = []; + + $table = code_snippets()->db->get_table_name(); + $base_dir = self::get_base_dir( $table, 'php' ); + $snippets_file_path = $base_dir . '/index.php'; + + if ( is_file( $snippets_file_path ) ) { + $site_snippets = is_file( $snippets_file_path ) ? require $snippets_file_path : []; + + $snippets[ $table ] = array_filter( + $site_snippets, + function ( $snippet ) { + return $snippet['active']; + } + ); + } + + if ( is_multisite() ) { + $root_base_dir = self::get_base_dir( $table ); + $active_shared_ids_file_path = $root_base_dir . '/active-shared-network-snippets.php'; + + $active_shared_ids = is_file( $active_shared_ids_file_path ) ? require $active_shared_ids_file_path : []; + $ms_table = code_snippets()->db->get_table_name( true ); + $ms_base_dir = self::get_base_dir( $ms_table, 'php' ); + $ms_snippets_file_path = $ms_base_dir . '/index.php'; + + if ( is_file( $ms_snippets_file_path ) ) { + $ms_snippets = is_file( $ms_snippets_file_path ) ? require $ms_snippets_file_path : []; + + $snippets[ $ms_table ] = array_filter( + $ms_snippets, + function ( $snippet ) use ( $active_shared_ids ) { + return $snippet['active'] || in_array( intval( $snippet['id'] ), $active_shared_ids, true ); + } + ); + } + } + + return $snippets; + } } diff --git a/src/php/snippet-ops.php b/src/php/snippet-ops.php index 39565c59..2eab3c94 100644 --- a/src/php/snippet-ops.php +++ b/src/php/snippet-ops.php @@ -675,12 +675,6 @@ function execute_active_snippets(): bool { return true; } -/** - * Execute the active snippets from the flat files. - * Read-write-execute operation. - * - * @return bool true on success, false on failure. - */ function execute_active_snippets_from_flat_files(): bool { global $wpdb; @@ -691,8 +685,8 @@ function execute_active_snippets_from_flat_files(): bool { } $db = code_snippets()->db; - $tables = $db->get_active_tables(); $scopes = array( 'global', 'single-use', is_admin() ? 'admin' : 'front-end' ); + $data = Snippet_Files::get_active_snippets_from_flat_files(); // Detect if a snippet is currently being edited, and if so, spare it from execution. $edit_id = 0; @@ -713,29 +707,14 @@ function execute_active_snippets_from_flat_files(): bool { } } - foreach ( $tables as $table_name ) { + foreach ( $data as $table_name => $active_snippets ) { $base_dir = Snippet_Files::get_base_dir( $table_name, 'php' ); + $active_snippets = cs_sort_snippets_by_priority( $active_snippets ); - if ( ! is_dir( $base_dir ) ) { - continue; - } - - $active_snippets_file_path = $base_dir . '/index.php'; - if ( ! is_file( $active_snippets_file_path ) ) { - continue; - } - - $active_snippets = require $active_snippets_file_path; - $sorted_snippets = cs_sort_snippets_by_priority( $active_snippets ); - - foreach ( $sorted_snippets as $snippet_id => $snippet ) { - if ( ! in_array( $snippet['scope'], $scopes, true ) ) { - continue; - } - - if ( ! $snippet['active'] ) { - continue; - } + // Loop through the returned snippets and execute the PHP code. + foreach ( $active_snippets as $snippet ) { + $snippet_id = intval( $snippet['id'] ); + $code = $snippet['code']; // If the snippet is a single-use snippet, deactivate it before execution to ensure that the process always happens. if ( 'single-use' === $snippet['scope'] ) { @@ -761,9 +740,9 @@ function execute_active_snippets_from_flat_files(): bool { } } - $file = $base_dir . '/' . $snippet_id . '.php'; if ( apply_filters( 'code_snippets/allow_execute_snippet', true, $snippet_id, $table_name ) && ! ( $edit_id === $snippet_id && $table_name === $edit_table ) ) { + $file = $base_dir . '/' . $snippet_id . '.php'; execute_snippet_from_flat_file( $file, $snippet_id ); } } From 737b3cfe7d6230b2c25417bb006057094ad6166c Mon Sep 17 00:00:00 2001 From: Louis Wolmarans Date: Wed, 2 Jul 2025 23:37:42 +0200 Subject: [PATCH 019/318] wip --- src/php/class-snippet-files.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/php/class-snippet-files.php b/src/php/class-snippet-files.php index 4fc0bab0..9bb7bdd6 100644 --- a/src/php/class-snippet-files.php +++ b/src/php/class-snippet-files.php @@ -232,10 +232,6 @@ function ( $snippet ) { } if ( is_multisite() ) { - $root_base_dir = self::get_base_dir( $table ); - $active_shared_ids_file_path = $root_base_dir . '/active-shared-network-snippets.php'; - - $active_shared_ids = is_file( $active_shared_ids_file_path ) ? require $active_shared_ids_file_path : []; $ms_table = code_snippets()->db->get_table_name( true ); $ms_base_dir = self::get_base_dir( $ms_table, 'php' ); $ms_snippets_file_path = $ms_base_dir . '/index.php'; @@ -243,6 +239,10 @@ function ( $snippet ) { if ( is_file( $ms_snippets_file_path ) ) { $ms_snippets = is_file( $ms_snippets_file_path ) ? require $ms_snippets_file_path : []; + $root_base_dir = self::get_base_dir( $table ); + $active_shared_ids_file_path = $root_base_dir . '/active-shared-network-snippets.php'; + $active_shared_ids = is_file( $active_shared_ids_file_path ) ? require $active_shared_ids_file_path : []; + $snippets[ $ms_table ] = array_filter( $ms_snippets, function ( $snippet ) use ( $active_shared_ids ) { From 20ec1896523dd192ab305bba28ec62896685e442 Mon Sep 17 00:00:00 2001 From: Louis Wolmarans Date: Wed, 2 Jul 2025 23:41:08 +0200 Subject: [PATCH 020/318] wip --- src/php/class-db.php | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/php/class-db.php b/src/php/class-db.php index a0b50782..64ea5f47 100644 --- a/src/php/class-db.php +++ b/src/php/class-db.php @@ -283,14 +283,4 @@ function ( $snippet ) use ( $active_shared_ids ) { return $active_snippets; } - - public function get_active_tables(): array { - $active_tables = array( $this->table ); - - if ( is_multisite() ) { - $active_tables[] = $this->ms_table; - } - - return $active_tables; - } } From 04e706d756eeb215921d38c830a56663efe22003 Mon Sep 17 00:00:00 2001 From: Louis Wolmarans Date: Wed, 2 Jul 2025 23:46:21 +0200 Subject: [PATCH 021/318] wip --- src/php/snippet-ops.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/php/snippet-ops.php b/src/php/snippet-ops.php index 2eab3c94..8fc90ff2 100644 --- a/src/php/snippet-ops.php +++ b/src/php/snippet-ops.php @@ -685,7 +685,7 @@ function execute_active_snippets_from_flat_files(): bool { } $db = code_snippets()->db; - $scopes = array( 'global', 'single-use', is_admin() ? 'admin' : 'front-end' ); + $scopes = [ 'global', 'single-use', is_admin() ? 'admin' : 'front-end' ]; $data = Snippet_Files::get_active_snippets_from_flat_files(); // Detect if a snippet is currently being edited, and if so, spare it from execution. @@ -713,8 +713,11 @@ function execute_active_snippets_from_flat_files(): bool { // Loop through the returned snippets and execute the PHP code. foreach ( $active_snippets as $snippet ) { + if ( ! in_array( $snippet['scope'], $scopes, true ) ) { + continue; + } + $snippet_id = intval( $snippet['id'] ); - $code = $snippet['code']; // If the snippet is a single-use snippet, deactivate it before execution to ensure that the process always happens. if ( 'single-use' === $snippet['scope'] ) { From 890481ae74afc246e4b6debe10c3a784d02f089d Mon Sep 17 00:00:00 2001 From: Louis Wolmarans Date: Sat, 5 Jul 2025 23:40:40 +0200 Subject: [PATCH 022/318] wip --- src/php/class-plugin.php | 9 +- .../classes/class-file-system-adapter.php | 35 ++++++++ .../classes}/class-snippet-files.php | 86 +++++++------------ .../files/handlers/html-snippet-handler.php | 17 ++++ .../files/handlers/php-snippet-handler.php | 17 ++++ .../interfaces/interface-file-system.php | 11 +++ .../interfaces/interface-snippet-handler.php | 9 ++ src/php/files/load.php | 9 ++ src/php/files/registry.php | 47 ++++++++++ 9 files changed, 183 insertions(+), 57 deletions(-) create mode 100644 src/php/files/classes/class-file-system-adapter.php rename src/php/{ => files/classes}/class-snippet-files.php (78%) create mode 100644 src/php/files/handlers/html-snippet-handler.php create mode 100644 src/php/files/handlers/php-snippet-handler.php create mode 100644 src/php/files/interfaces/interface-file-system.php create mode 100644 src/php/files/interfaces/interface-snippet-handler.php create mode 100644 src/php/files/load.php create mode 100644 src/php/files/registry.php diff --git a/src/php/class-plugin.php b/src/php/class-plugin.php index fda65d6f..c09edbb4 100644 --- a/src/php/class-plugin.php +++ b/src/php/class-plugin.php @@ -120,8 +120,13 @@ public function load_plugin() { require_once $includes_path . '/cloud/list-table-shared-ops.php'; // Snippet files. - require_once $includes_path . '/class-snippet-files.php'; - ( new Snippet_Files() )->init(); + require_once $includes_path . '/files/load.php'; + $registry = new Snippet_Handler_Registry( [ + 'php' => new Php_Snippet_Handler(), + 'html' => new Html_Snippet_Handler(), + ] ); + $fs = new WordPress_Filesystem_Adapter(); + ( new Snippet_Files( $registry, $fs ) )->register_hooks(); $this->active_snippets = new Active_Snippets(); $this->front_end = new Front_End(); diff --git a/src/php/files/classes/class-file-system-adapter.php b/src/php/files/classes/class-file-system-adapter.php new file mode 100644 index 00000000..b22308e3 --- /dev/null +++ b/src/php/files/classes/class-file-system-adapter.php @@ -0,0 +1,35 @@ +fs = $wp_filesystem; + } + + public function put_contents( string $path, string $contents, $chmod ) { + return $this->fs->put_contents( $path, $contents, $chmod ); + } + + public function exists( string $path ): bool { + return $this->fs->exists( $path ); + } + + public function delete( string $path ): bool { + return $this->fs->delete( $path ); + } + + public function is_dir( string $path ): bool { + return $this->fs->is_dir( $path ); + } + + public function mkdir( string $path, $chmod ) { + return $this->fs->mkdir( $path, $chmod ); + } +} diff --git a/src/php/class-snippet-files.php b/src/php/files/classes/class-snippet-files.php similarity index 78% rename from src/php/class-snippet-files.php rename to src/php/files/classes/class-snippet-files.php index 9bb7bdd6..a7a0df61 100644 --- a/src/php/class-snippet-files.php +++ b/src/php/files/classes/class-snippet-files.php @@ -4,19 +4,17 @@ class Snippet_Files { - /** - * Holds the WP_Filesystem instance. - * - * @var \WP_Filesystem_Base - */ - private $fs; + private Snippet_Handler_Registry $handler_registry; - const TYPES_TO_HANDLE = [ 'php', 'html' ]; + private File_System_Interface $fs; - public function init() { - $this->ensure_filesystem(); - $this->register_hooks(); - } + public function __construct( + Snippet_Handler_Registry $handler_registry, + File_System_Interface $fs + ) { + $this->handler_registry = $handler_registry; + $this->fs = $fs; + } public function register_hooks() { add_action( 'code_snippets/create_snippet', [ $this, 'handle_snippet' ], 10, 2 ); @@ -27,48 +25,36 @@ public function register_hooks() { add_action( 'updated_option', [ $this, 'sync_active_shared_network_snippets' ], 10, 3 ); } - private function ensure_filesystem() { - if ( ! $this->fs ) { - if ( ! function_exists( 'WP_Filesystem' ) ) { - require_once ABSPATH . 'wp-admin/includes/file.php'; - } - - WP_Filesystem(); - - global $wp_filesystem; - - $this->fs = $wp_filesystem; - } - } - - private function should_handle_snippet( string $snippet_type ) { - return in_array( $snippet_type, self::TYPES_TO_HANDLE, true ); - } - public function handle_snippet( Snippet $snippet, string $table ) { $snippet_type = $snippet->get_type(); - if ( ! $this->should_handle_snippet( $snippet_type ) ) { + $handler = $this->handler_registry->get_handler( $snippet_type ); + + if ( ! $handler ) { return; } - $base_dir = self::get_base_dir( $table, $snippet_type ); + $base_dir = self::get_base_dir( $table, $handler->get_dir_name() ); $this->maybe_create_directory( $base_dir ); $file_path = $this->get_snippet_file_path( $base_dir, $snippet->id ); - $this->write_snippet_file( $file_path, $snippet->code, $snippet_type ); + $contents = $handler->wrap_code( $snippet->code ); + + $this->fs->put_contents( $file_path, $contents, FS_CHMOD_FILE ); $this->update_config_file( $base_dir, $snippet ); } public function delete_snippet( Snippet $snippet, bool $network ) { $snippet_type = $snippet->get_type(); - if ( ! $this->should_handle_snippet( $snippet_type ) ) { + $handler = $this->handler_registry->get_handler( $snippet_type ); + + if ( ! $handler ) { return; } $table = code_snippets()->db->get_table_name( $network ); - $base_dir = self::get_base_dir( $table, $snippet_type ); + $base_dir = self::get_base_dir( $table, $handler->get_dir_name() ); $file_path = $this->get_snippet_file_path( $base_dir, $snippet->id ); $this->delete_file( $file_path ); @@ -78,19 +64,23 @@ public function delete_snippet( Snippet $snippet, bool $network ) { public function activate_snippet( Snippet $snippet, bool $network ) { $snippet = get_snippet( $snippet->id, $network ); - $snippet_type = $snippet->get_type(); - if ( ! $this->should_handle_snippet( $snippet_type ) ) { + $handler = $this->handler_registry->get_handler( $snippet_type ); + + if ( ! $handler ) { return; } $table = code_snippets()->db->get_table_name( $network ); - $base_dir = self::get_base_dir( $table, $snippet_type ); + $base_dir = self::get_base_dir( $table, $handler->get_dir_name() ); $this->maybe_create_directory( $base_dir ); $file_path = $this->get_snippet_file_path( $base_dir, $snippet->id ); - $this->write_snippet_file( $file_path, $snippet->code, $snippet_type ); + + $contents = $handler->wrap_code( $snippet->code ); + + $this->fs->put_contents( $file_path, $contents, FS_CHMOD_FILE ); $this->update_config_file( $base_dir, $snippet ); } @@ -98,13 +88,14 @@ public function activate_snippet( Snippet $snippet, bool $network ) { public function deactivate_snippet( int $snippet_id, bool $network ) { $snippet = get_snippet( $snippet_id, $network ); $snippet_type = $snippet->get_type(); + $handler = $this->handler_registry->get_handler( $snippet_type ); - if ( ! $this->should_handle_snippet( $snippet_type ) ) { + if ( ! $handler ) { return; } $table = code_snippets()->db->get_table_name( $network ); - $base_dir = self::get_base_dir( $table, $snippet_type ); + $base_dir = self::get_base_dir( $table, $handler->get_dir_name() ); $this->update_config_file( $base_dir, $snippet ); } @@ -142,21 +133,6 @@ private function get_snippet_file_path( string $base_dir, int $snippet_id ) { return trailingslashit( $base_dir ) . $snippet_id . '.php'; } - /** - * Writes the snippet code to a file, with the required header. - */ - private function write_snippet_file( string $file_path, string $code, string $snippet_type ) { - $content = "\n\n"; - } - - $content .= $code; - - $this->fs->put_contents( $file_path, $content, FS_CHMOD_FILE ); - } - /** * Deletes a file if it exists. */ diff --git a/src/php/files/handlers/html-snippet-handler.php b/src/php/files/handlers/html-snippet-handler.php new file mode 100644 index 00000000..7916297d --- /dev/null +++ b/src/php/files/handlers/html-snippet-handler.php @@ -0,0 +1,17 @@ +\n\n" . $code; + } +} diff --git a/src/php/files/handlers/php-snippet-handler.php b/src/php/files/handlers/php-snippet-handler.php new file mode 100644 index 00000000..666617b0 --- /dev/null +++ b/src/php/files/handlers/php-snippet-handler.php @@ -0,0 +1,17 @@ + $handler ) { + $this->register_handler( $type, $handler ); + } + } + + /** + * Registers a handler for a snippet type. + * + * @param string $type + * @param Snippet_Type_Handler_Interface $handler + * @return void + */ + public function register_handler( string $type, Snippet_Type_Handler_Interface $handler ): void { + $this->handlers[ $type ] = $handler; + } + + /** + * Gets the handler for a snippet type. + * + * @param string $type + * + * @return Snippet_Type_Handler_Interface|null + */ + public function get_handler( string $type ): ?Snippet_Type_Handler_Interface { + if ( ! isset( $this->handlers[ $type ] ) ) { + return null; + } + + return $this->handlers[ $type ]; + } +} From 3f5eea3fb3ddeebceb6a9a4d51939ccdd02930af Mon Sep 17 00:00:00 2001 From: Louis Wolmarans Date: Sat, 5 Jul 2025 23:44:39 +0200 Subject: [PATCH 023/318] wip --- src/php/class-plugin.php | 2 +- .../{files => flat-files}/classes/class-file-system-adapter.php | 0 src/php/{files => flat-files}/classes/class-snippet-files.php | 0 src/php/{files => flat-files}/handlers/html-snippet-handler.php | 0 src/php/{files => flat-files}/handlers/php-snippet-handler.php | 0 .../{files => flat-files}/interfaces/interface-file-system.php | 0 .../interfaces/interface-snippet-handler.php | 0 src/php/{files => flat-files}/load.php | 0 src/php/{files => flat-files}/registry.php | 0 9 files changed, 1 insertion(+), 1 deletion(-) rename src/php/{files => flat-files}/classes/class-file-system-adapter.php (100%) rename src/php/{files => flat-files}/classes/class-snippet-files.php (100%) rename src/php/{files => flat-files}/handlers/html-snippet-handler.php (100%) rename src/php/{files => flat-files}/handlers/php-snippet-handler.php (100%) rename src/php/{files => flat-files}/interfaces/interface-file-system.php (100%) rename src/php/{files => flat-files}/interfaces/interface-snippet-handler.php (100%) rename src/php/{files => flat-files}/load.php (100%) rename src/php/{files => flat-files}/registry.php (100%) diff --git a/src/php/class-plugin.php b/src/php/class-plugin.php index c09edbb4..ed07aaec 100644 --- a/src/php/class-plugin.php +++ b/src/php/class-plugin.php @@ -120,7 +120,7 @@ public function load_plugin() { require_once $includes_path . '/cloud/list-table-shared-ops.php'; // Snippet files. - require_once $includes_path . '/files/load.php'; + require_once $includes_path . '/flat-files/load.php'; $registry = new Snippet_Handler_Registry( [ 'php' => new Php_Snippet_Handler(), 'html' => new Html_Snippet_Handler(), diff --git a/src/php/files/classes/class-file-system-adapter.php b/src/php/flat-files/classes/class-file-system-adapter.php similarity index 100% rename from src/php/files/classes/class-file-system-adapter.php rename to src/php/flat-files/classes/class-file-system-adapter.php diff --git a/src/php/files/classes/class-snippet-files.php b/src/php/flat-files/classes/class-snippet-files.php similarity index 100% rename from src/php/files/classes/class-snippet-files.php rename to src/php/flat-files/classes/class-snippet-files.php diff --git a/src/php/files/handlers/html-snippet-handler.php b/src/php/flat-files/handlers/html-snippet-handler.php similarity index 100% rename from src/php/files/handlers/html-snippet-handler.php rename to src/php/flat-files/handlers/html-snippet-handler.php diff --git a/src/php/files/handlers/php-snippet-handler.php b/src/php/flat-files/handlers/php-snippet-handler.php similarity index 100% rename from src/php/files/handlers/php-snippet-handler.php rename to src/php/flat-files/handlers/php-snippet-handler.php diff --git a/src/php/files/interfaces/interface-file-system.php b/src/php/flat-files/interfaces/interface-file-system.php similarity index 100% rename from src/php/files/interfaces/interface-file-system.php rename to src/php/flat-files/interfaces/interface-file-system.php diff --git a/src/php/files/interfaces/interface-snippet-handler.php b/src/php/flat-files/interfaces/interface-snippet-handler.php similarity index 100% rename from src/php/files/interfaces/interface-snippet-handler.php rename to src/php/flat-files/interfaces/interface-snippet-handler.php diff --git a/src/php/files/load.php b/src/php/flat-files/load.php similarity index 100% rename from src/php/files/load.php rename to src/php/flat-files/load.php diff --git a/src/php/files/registry.php b/src/php/flat-files/registry.php similarity index 100% rename from src/php/files/registry.php rename to src/php/flat-files/registry.php From 69449a01470195e85102078a1dcfe55a3b1e9ebb Mon Sep 17 00:00:00 2001 From: Louis Wolmarans Date: Sat, 5 Jul 2025 23:48:38 +0200 Subject: [PATCH 024/318] wip --- .../classes/class-file-system-adapter.php | 48 ++++++------ .../handlers/html-snippet-handler.php | 18 ++--- .../interfaces/interface-file-system.php | 10 +-- .../interfaces/interface-snippet-handler.php | 6 +- src/php/flat-files/registry.php | 74 +++++++++---------- 5 files changed, 78 insertions(+), 78 deletions(-) diff --git a/src/php/flat-files/classes/class-file-system-adapter.php b/src/php/flat-files/classes/class-file-system-adapter.php index b22308e3..c8085e5c 100644 --- a/src/php/flat-files/classes/class-file-system-adapter.php +++ b/src/php/flat-files/classes/class-file-system-adapter.php @@ -2,34 +2,34 @@ namespace Code_Snippets; class WordPress_Filesystem_Adapter implements File_System_Interface { - private $fs; + private $fs; - public function __construct() { - if ( ! function_exists( 'WP_Filesystem' ) ) { - require_once ABSPATH . 'wp-admin/includes/file.php'; - } - WP_Filesystem(); - global $wp_filesystem; - $this->fs = $wp_filesystem; - } + public function __construct() { + if ( ! function_exists( 'WP_Filesystem' ) ) { + require_once ABSPATH . 'wp-admin/includes/file.php'; + } + WP_Filesystem(); + global $wp_filesystem; + $this->fs = $wp_filesystem; + } - public function put_contents( string $path, string $contents, $chmod ) { - return $this->fs->put_contents( $path, $contents, $chmod ); - } + public function put_contents( string $path, string $contents, $chmod ) { + return $this->fs->put_contents( $path, $contents, $chmod ); + } - public function exists( string $path ): bool { - return $this->fs->exists( $path ); - } + public function exists( string $path ): bool { + return $this->fs->exists( $path ); + } - public function delete( string $path ): bool { - return $this->fs->delete( $path ); - } + public function delete( string $path ): bool { + return $this->fs->delete( $path ); + } - public function is_dir( string $path ): bool { - return $this->fs->is_dir( $path ); - } + public function is_dir( string $path ): bool { + return $this->fs->is_dir( $path ); + } - public function mkdir( string $path, $chmod ) { - return $this->fs->mkdir( $path, $chmod ); - } + public function mkdir( string $path, $chmod ) { + return $this->fs->mkdir( $path, $chmod ); + } } diff --git a/src/php/flat-files/handlers/html-snippet-handler.php b/src/php/flat-files/handlers/html-snippet-handler.php index 7916297d..d7a4446a 100644 --- a/src/php/flat-files/handlers/html-snippet-handler.php +++ b/src/php/flat-files/handlers/html-snippet-handler.php @@ -3,15 +3,15 @@ namespace Code_Snippets; class Html_Snippet_Handler implements Snippet_Type_Handler_Interface { - public function get_file_extension(): string { - return 'php'; - } + public function get_file_extension(): string { + return 'php'; + } - public function get_dir_name(): string { - return 'html'; - } + public function get_dir_name(): string { + return 'html'; + } - public function wrap_code( string $code ): string { - return "\n\n" . $code; - } + public function wrap_code( string $code ): string { + return "\n\n" . $code; + } } diff --git a/src/php/flat-files/interfaces/interface-file-system.php b/src/php/flat-files/interfaces/interface-file-system.php index 3a8cc7a3..38ab5bd0 100644 --- a/src/php/flat-files/interfaces/interface-file-system.php +++ b/src/php/flat-files/interfaces/interface-file-system.php @@ -3,9 +3,9 @@ namespace Code_Snippets; interface File_System_Interface { - public function put_contents( string $path, string $contents, $chmod ); - public function exists( string $path ): bool; - public function delete( string $path ): bool; - public function is_dir( string $path ): bool; - public function mkdir( string $path, $chmod ); + public function put_contents( string $path, string $contents, $chmod ); + public function exists( string $path ): bool; + public function delete( string $path ): bool; + public function is_dir( string $path ): bool; + public function mkdir( string $path, $chmod ); } diff --git a/src/php/flat-files/interfaces/interface-snippet-handler.php b/src/php/flat-files/interfaces/interface-snippet-handler.php index fdcbb6b8..4ff024cb 100644 --- a/src/php/flat-files/interfaces/interface-snippet-handler.php +++ b/src/php/flat-files/interfaces/interface-snippet-handler.php @@ -3,7 +3,7 @@ namespace Code_Snippets; interface Snippet_Type_Handler_Interface { - public function get_file_extension(): string; - public function get_dir_name(): string; - public function wrap_code( string $code ): string; + public function get_file_extension(): string; + public function get_dir_name(): string; + public function wrap_code( string $code ): string; } diff --git a/src/php/flat-files/registry.php b/src/php/flat-files/registry.php index a02b589b..4fce503d 100644 --- a/src/php/flat-files/registry.php +++ b/src/php/flat-files/registry.php @@ -3,45 +3,45 @@ namespace Code_Snippets; class Snippet_Handler_Registry { - /** - * @var Snippet_Type_Handler_Interface[] - */ - private array $handlers = []; + /** + * @var Snippet_Type_Handler_Interface[] + */ + private array $handlers = []; - /** - * Constructor - * - * @param Snippet_Type_Handler_Interface[] $handlers - */ - public function __construct( array $handlers ) { - foreach ( $handlers as $type => $handler ) { - $this->register_handler( $type, $handler ); - } - } + /** + * Constructor + * + * @param Snippet_Type_Handler_Interface[] $handlers + */ + public function __construct( array $handlers ) { + foreach ( $handlers as $type => $handler ) { + $this->register_handler( $type, $handler ); + } + } - /** - * Registers a handler for a snippet type. - * - * @param string $type - * @param Snippet_Type_Handler_Interface $handler - * @return void - */ - public function register_handler( string $type, Snippet_Type_Handler_Interface $handler ): void { - $this->handlers[ $type ] = $handler; - } + /** + * Registers a handler for a snippet type. + * + * @param string $type + * @param Snippet_Type_Handler_Interface $handler + * @return void + */ + public function register_handler( string $type, Snippet_Type_Handler_Interface $handler ): void { + $this->handlers[ $type ] = $handler; + } - /** - * Gets the handler for a snippet type. - * - * @param string $type - * - * @return Snippet_Type_Handler_Interface|null - */ - public function get_handler( string $type ): ?Snippet_Type_Handler_Interface { - if ( ! isset( $this->handlers[ $type ] ) ) { - return null; - } + /** + * Gets the handler for a snippet type. + * + * @param string $type + * + * @return Snippet_Type_Handler_Interface|null + */ + public function get_handler( string $type ): ?Snippet_Type_Handler_Interface { + if ( ! isset( $this->handlers[ $type ] ) ) { + return null; + } - return $this->handlers[ $type ]; - } + return $this->handlers[ $type ]; + } } From 91b754696d144c0c119b89482aa8dfcecb6bd342 Mon Sep 17 00:00:00 2001 From: Louis Wolmarans Date: Sat, 5 Jul 2025 23:49:37 +0200 Subject: [PATCH 025/318] wip --- src/php/flat-files/classes/class-snippet-files.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/php/flat-files/classes/class-snippet-files.php b/src/php/flat-files/classes/class-snippet-files.php index a7a0df61..9750d2a2 100644 --- a/src/php/flat-files/classes/class-snippet-files.php +++ b/src/php/flat-files/classes/class-snippet-files.php @@ -8,13 +8,13 @@ class Snippet_Files { private File_System_Interface $fs; - public function __construct( - Snippet_Handler_Registry $handler_registry, + public function __construct( + Snippet_Handler_Registry $handler_registry, File_System_Interface $fs - ) { - $this->handler_registry = $handler_registry; - $this->fs = $fs; - } + ) { + $this->handler_registry = $handler_registry; + $this->fs = $fs; + } public function register_hooks() { add_action( 'code_snippets/create_snippet', [ $this, 'handle_snippet' ], 10, 2 ); From 0029e0f0dde5f386acaf1b1695f4dc0a8baa0f89 Mon Sep 17 00:00:00 2001 From: Louis Wolmarans Date: Sat, 5 Jul 2025 23:55:22 +0200 Subject: [PATCH 026/318] wip --- src/php/flat-files/classes/class-snippet-files.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/php/flat-files/classes/class-snippet-files.php b/src/php/flat-files/classes/class-snippet-files.php index 9750d2a2..add308ad 100644 --- a/src/php/flat-files/classes/class-snippet-files.php +++ b/src/php/flat-files/classes/class-snippet-files.php @@ -36,7 +36,7 @@ public function handle_snippet( Snippet $snippet, string $table ) { $base_dir = self::get_base_dir( $table, $handler->get_dir_name() ); $this->maybe_create_directory( $base_dir ); - $file_path = $this->get_snippet_file_path( $base_dir, $snippet->id ); + $file_path = $this->get_snippet_file_path( $base_dir, $snippet->id, $handler->get_file_extension() ); $contents = $handler->wrap_code( $snippet->code ); @@ -56,7 +56,7 @@ public function delete_snippet( Snippet $snippet, bool $network ) { $table = code_snippets()->db->get_table_name( $network ); $base_dir = self::get_base_dir( $table, $handler->get_dir_name() ); - $file_path = $this->get_snippet_file_path( $base_dir, $snippet->id ); + $file_path = $this->get_snippet_file_path( $base_dir, $snippet->id, $handler->get_file_extension() ); $this->delete_file( $file_path ); $this->update_config_file( $base_dir, $snippet, true ); @@ -76,7 +76,7 @@ public function activate_snippet( Snippet $snippet, bool $network ) { $this->maybe_create_directory( $base_dir ); - $file_path = $this->get_snippet_file_path( $base_dir, $snippet->id ); + $file_path = $this->get_snippet_file_path( $base_dir, $snippet->id, $handler->get_file_extension() ); $contents = $handler->wrap_code( $snippet->code ); @@ -129,8 +129,8 @@ private function maybe_create_directory( string $dir ) { /** * Returns the path to the snippet PHP file. */ - private function get_snippet_file_path( string $base_dir, int $snippet_id ) { - return trailingslashit( $base_dir ) . $snippet_id . '.php'; + private function get_snippet_file_path( string $base_dir, int $snippet_id, string $ext ) { + return trailingslashit( $base_dir ) . $snippet_id . '.' . $ext; } /** From 1a7789e9792923dba658880188f7b00d961c10a6 Mon Sep 17 00:00:00 2001 From: Louis Wolmarans Date: Sun, 6 Jul 2025 00:07:32 +0200 Subject: [PATCH 027/318] wip --- src/php/class-plugin.php | 7 ++- .../classes/class-config-repository.php | 41 +++++++++++++ .../classes/class-snippet-files.php | 57 +++---------------- .../interface-config-repository.php | 9 +++ src/php/flat-files/load.php | 1 + 5 files changed, 66 insertions(+), 49 deletions(-) create mode 100644 src/php/flat-files/classes/class-config-repository.php create mode 100644 src/php/flat-files/interfaces/interface-config-repository.php diff --git a/src/php/class-plugin.php b/src/php/class-plugin.php index ed07aaec..9366a5f8 100644 --- a/src/php/class-plugin.php +++ b/src/php/class-plugin.php @@ -121,12 +121,17 @@ public function load_plugin() { // Snippet files. require_once $includes_path . '/flat-files/load.php'; + $registry = new Snippet_Handler_Registry( [ 'php' => new Php_Snippet_Handler(), 'html' => new Html_Snippet_Handler(), ] ); + $fs = new WordPress_Filesystem_Adapter(); - ( new Snippet_Files( $registry, $fs ) )->register_hooks(); + + $config_repo = new Snippet_Config_Repository( $fs ); + + ( new Snippet_Files( $registry, $fs, $config_repo ) )->register_hooks(); $this->active_snippets = new Active_Snippets(); $this->front_end = new Front_End(); diff --git a/src/php/flat-files/classes/class-config-repository.php b/src/php/flat-files/classes/class-config-repository.php new file mode 100644 index 00000000..bcf48535 --- /dev/null +++ b/src/php/flat-files/classes/class-config-repository.php @@ -0,0 +1,41 @@ +fs = $fs; + } + + public function load( string $base_dir ): array { + $config_file_path = trailingslashit( $base_dir ) . 'index.php'; + + return is_file( $config_file_path ) + ? require $config_file_path + : []; + } + + public function save( string $base_dir, array $active_snippets ): void { + $config_file_path = trailingslashit( $base_dir ) . 'index.php'; + + $file_content = "fs->put_contents( $config_file_path, $file_content, FS_CHMOD_FILE ); + } + + public function update( string $base_dir, Snippet $snippet, ?bool $remove = false ): void { + $active_snippets = $this->load( $base_dir ); + + if ( ! $remove ) { + $active_snippets[ $snippet->id ] = $snippet->get_fields(); + } else { + unset( $active_snippets[ $snippet->id ] ); + } + + $this->save( $base_dir, $active_snippets ); + } +} diff --git a/src/php/flat-files/classes/class-snippet-files.php b/src/php/flat-files/classes/class-snippet-files.php index add308ad..35ccaa03 100644 --- a/src/php/flat-files/classes/class-snippet-files.php +++ b/src/php/flat-files/classes/class-snippet-files.php @@ -8,12 +8,16 @@ class Snippet_Files { private File_System_Interface $fs; + private Snippet_Config_Repository_Interface $config_repo; + public function __construct( Snippet_Handler_Registry $handler_registry, - File_System_Interface $fs + File_System_Interface $fs, + Snippet_Config_Repository_Interface $config_repo ) { $this->handler_registry = $handler_registry; $this->fs = $fs; + $this->config_repo = $config_repo; } public function register_hooks() { @@ -42,7 +46,7 @@ public function handle_snippet( Snippet $snippet, string $table ) { $this->fs->put_contents( $file_path, $contents, FS_CHMOD_FILE ); - $this->update_config_file( $base_dir, $snippet ); + $this->config_repo->update( $base_dir, $snippet ); } public function delete_snippet( Snippet $snippet, bool $network ) { @@ -59,7 +63,7 @@ public function delete_snippet( Snippet $snippet, bool $network ) { $file_path = $this->get_snippet_file_path( $base_dir, $snippet->id, $handler->get_file_extension() ); $this->delete_file( $file_path ); - $this->update_config_file( $base_dir, $snippet, true ); + $this->config_repo->update( $base_dir, $snippet, true ); } public function activate_snippet( Snippet $snippet, bool $network ) { @@ -82,7 +86,7 @@ public function activate_snippet( Snippet $snippet, bool $network ) { $this->fs->put_contents( $file_path, $contents, FS_CHMOD_FILE ); - $this->update_config_file( $base_dir, $snippet ); + $this->config_repo->update( $base_dir, $snippet ); } public function deactivate_snippet( int $snippet_id, bool $network ) { @@ -97,12 +101,9 @@ public function deactivate_snippet( int $snippet_id, bool $network ) { $table = code_snippets()->db->get_table_name( $network ); $base_dir = self::get_base_dir( $table, $handler->get_dir_name() ); - $this->update_config_file( $base_dir, $snippet ); + $this->config_repo->update( $base_dir, $snippet ); } - /** - * Returns the base directory path for a given context. - */ public static function get_base_dir( string $table = '', string $snippet_type = '' ) { $base_dir = WP_CONTENT_DIR . '/code-snippets'; @@ -117,62 +118,22 @@ public static function get_base_dir( string $table = '', string $snippet_type = return $base_dir; } - /** - * Creates the directory if it does not exist. - */ private function maybe_create_directory( string $dir ) { if ( ! $this->fs->is_dir( $dir ) ) { $this->fs->mkdir( $dir, FS_CHMOD_DIR ); } } - /** - * Returns the path to the snippet PHP file. - */ private function get_snippet_file_path( string $base_dir, int $snippet_id, string $ext ) { return trailingslashit( $base_dir ) . $snippet_id . '.' . $ext; } - /** - * Deletes a file if it exists. - */ private function delete_file( string $file_path ) { if ( $this->fs->exists( $file_path ) ) { $this->fs->delete( $file_path ); } } - /** - * Loads the index.php array by requiring it directly. - */ - private function load_config_file( string $config_file_path ) { - return is_file( $config_file_path ) ? require $config_file_path : []; - } - - /** - * Saves the index.php file via WP_Filesystem. - */ - private function save_config_file( string $config_file_path, array $active_snippets ) { - $file_content = "fs->put_contents( $config_file_path, $file_content, FS_CHMOD_FILE ); - } - - /** - * Updates the index.php file with snippet config. - */ - private function update_config_file( string $base_dir, Snippet $snippet, ?bool $remove = false ) { - $config_file_path = trailingslashit( $base_dir ) . 'index.php'; - $active_snippets = $this->load_config_file( $config_file_path ); - - if ( ! $remove ) { - $active_snippets[ $snippet->id ] = $snippet->get_fields(); - } else { - unset( $active_snippets[ $snippet->id ] ); - } - - $this->save_config_file( $config_file_path, $active_snippets ); - } - public function sync_active_shared_network_snippets( $option, $old_value, $value ) { if ( 'active_shared_network_snippets' !== $option ) { return; diff --git a/src/php/flat-files/interfaces/interface-config-repository.php b/src/php/flat-files/interfaces/interface-config-repository.php new file mode 100644 index 00000000..e8c3a819 --- /dev/null +++ b/src/php/flat-files/interfaces/interface-config-repository.php @@ -0,0 +1,9 @@ + Date: Sun, 6 Jul 2025 00:08:36 +0200 Subject: [PATCH 028/318] wip --- src/php/flat-files/load.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/php/flat-files/load.php b/src/php/flat-files/load.php index ea42c050..476a5113 100644 --- a/src/php/flat-files/load.php +++ b/src/php/flat-files/load.php @@ -8,3 +8,4 @@ require_once 'handlers/html-snippet-handler.php'; require_once 'classes/class-file-system-adapter.php'; require_once 'classes/class-snippet-files.php'; +require_once 'classes/class-config-repository.php'; From 87b9b2dca9ae96755e4c0951489b8579ce4958ee Mon Sep 17 00:00:00 2001 From: Louis Wolmarans Date: Sun, 6 Jul 2025 00:27:08 +0200 Subject: [PATCH 029/318] wip --- src/php/flat-files/load.php | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/php/flat-files/load.php b/src/php/flat-files/load.php index 476a5113..dd87d808 100644 --- a/src/php/flat-files/load.php +++ b/src/php/flat-files/load.php @@ -1,11 +1,17 @@ Date: Sun, 6 Jul 2025 21:51:13 +0200 Subject: [PATCH 030/318] wip --- src/php/flat-files/classes/class-snippet-files.php | 13 +++++++++++++ src/php/load.php | 7 +++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/php/flat-files/classes/class-snippet-files.php b/src/php/flat-files/classes/class-snippet-files.php index 35ccaa03..093e33fd 100644 --- a/src/php/flat-files/classes/class-snippet-files.php +++ b/src/php/flat-files/classes/class-snippet-files.php @@ -26,7 +26,10 @@ public function register_hooks() { add_action( 'code_snippets/delete_snippet', [ $this, 'delete_snippet' ], 10, 2 ); add_action( 'code_snippets/activate_snippet', [ $this, 'activate_snippet' ], 10, 2 ); add_action( 'code_snippets/deactivate_snippet', [ $this, 'deactivate_snippet' ], 10, 2 ); + add_action( 'updated_option', [ $this, 'sync_active_shared_network_snippets' ], 10, 3 ); + + add_filter( 'code_snippets_settings_fields', [ $this, 'add_settings_fields' ], 10, 1 ); } public function handle_snippet( Snippet $snippet, string $table ) { @@ -191,4 +194,14 @@ function ( $snippet ) use ( $active_shared_ids ) { return $snippets; } + + public function add_settings_fields( array $fields ) { + $fields['general']['enable_flat_files'] = [ + 'name' => __( 'Enable Flat Files', 'code-snippets' ), + 'type' => 'checkbox', + 'label' => __( 'Snippets will be executed from flat files instead of the database.', 'code-snippets' ), + ]; + + return $fields; + } } diff --git a/src/php/load.php b/src/php/load.php index 8700186a..220d95a5 100644 --- a/src/php/load.php +++ b/src/php/load.php @@ -65,5 +65,8 @@ function code_snippets(): Plugin { code_snippets()->load_plugin(); // Execute the snippets once the plugins are loaded. -// add_action( 'plugins_loaded', __NAMESPACE__ . '\execute_active_snippets', 1 ); -add_action( 'plugins_loaded', __NAMESPACE__ . '\execute_active_snippets_from_flat_files', 1 ); +$snippet_execution_fn = Settings\get_setting( 'general', 'enable_flat_files' ) + ? '\execute_active_snippets_from_flat_files' + : '\execute_active_snippets'; + +add_action( 'plugins_loaded', __NAMESPACE__ . $snippet_execution_fn, 1 ); From 531ee3515a14ff6f0e5723337194e9524fa6b59b Mon Sep 17 00:00:00 2001 From: Louis Wolmarans Date: Sun, 6 Jul 2025 21:57:51 +0200 Subject: [PATCH 031/318] wip --- src/php/front-end/class-front-end.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/php/front-end/class-front-end.php b/src/php/front-end/class-front-end.php index 60429b35..258b27ec 100644 --- a/src/php/front-end/class-front-end.php +++ b/src/php/front-end/class-front-end.php @@ -245,6 +245,12 @@ protected function evaluate_shortcode_content( Snippet $snippet, array $atts ): return $snippet->code; } + $should_execute_from_flat_files = Settings\get_setting( 'general', 'enable_flat_files' ); + + if ( ! $should_execute_from_flat_files ) { + return $this->evaluate_shortcode_from_db( $snippet, $atts ); + } + $network = DB::validate_network_param( $snippet->network ); $table_name = code_snippets()->db->get_table_name( $network ); $filepath = WP_CONTENT_DIR . '/code-snippets/' . $table_name . '/html/' . $snippet->id . '.php'; From 0b19d6ef3ef82408ff3b47cd1eec24bcae4cc850 Mon Sep 17 00:00:00 2001 From: Louis Wolmarans Date: Sun, 6 Jul 2025 22:07:48 +0200 Subject: [PATCH 032/318] wip --- src/php/class-plugin.php | 2 +- src/php/front-end/class-front-end.php | 22 ++++++++++++++++++++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/php/class-plugin.php b/src/php/class-plugin.php index 9366a5f8..bb21473a 100644 --- a/src/php/class-plugin.php +++ b/src/php/class-plugin.php @@ -134,7 +134,7 @@ public function load_plugin() { ( new Snippet_Files( $registry, $fs, $config_repo ) )->register_hooks(); $this->active_snippets = new Active_Snippets(); - $this->front_end = new Front_End(); + $this->front_end = new Front_End( $registry ); $this->cloud_api = new Cloud_API(); $upgrade = new Upgrade( $this->version, $this->db ); diff --git a/src/php/front-end/class-front-end.php b/src/php/front-end/class-front-end.php index 258b27ec..f0ddf8e1 100644 --- a/src/php/front-end/class-front-end.php +++ b/src/php/front-end/class-front-end.php @@ -33,10 +33,14 @@ class Front_End { */ const MAX_SHORTCODE_DEPTH = 5; + private Snippet_Handler_Registry $handler_registry; + /** * Class constructor */ - public function __construct() { + public function __construct( Snippet_Handler_Registry $handler_registry ) { + $this->handler_registry = $handler_registry; + add_action( 'the_posts', [ $this, 'enqueue_highlighting' ] ); add_action( 'init', [ $this, 'setup_mce_plugin' ] ); @@ -232,6 +236,20 @@ protected function convert_boolean_attribute_flags( array $atts, array $boolean_ return $atts; } + /** + * Build the file path for a snippet's flat file. + * + * @param string $table_name Table name for the snippet. + * @param Snippet $snippet Snippet object. + * + * @return string Full file path for the snippet. + */ + private function build_snippet_filepath( string $table_name, Snippet $snippet ): string { + $handler = $this->handler_registry->get_handler( $snippet->get_type() ); + + return WP_CONTENT_DIR . '/code-snippets/' . $table_name . '/' . $handler->get_dir_name() . '/' . $snippet->id . '.' . $handler->get_file_extension(); + } + /** * Evaluate the code from a content shortcode. * @@ -253,7 +271,7 @@ protected function evaluate_shortcode_content( Snippet $snippet, array $atts ): $network = DB::validate_network_param( $snippet->network ); $table_name = code_snippets()->db->get_table_name( $network ); - $filepath = WP_CONTENT_DIR . '/code-snippets/' . $table_name . '/html/' . $snippet->id . '.php'; + $filepath = $this->build_snippet_filepath( $table_name, $snippet ); return file_exists( $filepath ) ? $this->evaluate_shortcode_from_flat_file( $filepath, $atts ) From ff80d08a00934d88ab6ea02b2c6561dc355a278e Mon Sep 17 00:00:00 2001 From: Louis Wolmarans Date: Sun, 6 Jul 2025 22:10:29 +0200 Subject: [PATCH 033/318] wip --- src/php/front-end/class-front-end.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/php/front-end/class-front-end.php b/src/php/front-end/class-front-end.php index f0ddf8e1..66d00e05 100644 --- a/src/php/front-end/class-front-end.php +++ b/src/php/front-end/class-front-end.php @@ -244,7 +244,7 @@ protected function convert_boolean_attribute_flags( array $atts, array $boolean_ * * @return string Full file path for the snippet. */ - private function build_snippet_filepath( string $table_name, Snippet $snippet ): string { + private function build_snippet_flat_file_path( string $table_name, Snippet $snippet ): string { $handler = $this->handler_registry->get_handler( $snippet->get_type() ); return WP_CONTENT_DIR . '/code-snippets/' . $table_name . '/' . $handler->get_dir_name() . '/' . $snippet->id . '.' . $handler->get_file_extension(); @@ -271,7 +271,7 @@ protected function evaluate_shortcode_content( Snippet $snippet, array $atts ): $network = DB::validate_network_param( $snippet->network ); $table_name = code_snippets()->db->get_table_name( $network ); - $filepath = $this->build_snippet_filepath( $table_name, $snippet ); + $filepath = $this->build_snippet_flat_file_path( $table_name, $snippet ); return file_exists( $filepath ) ? $this->evaluate_shortcode_from_flat_file( $filepath, $atts ) From b533854ab4f5ae9501298e858fcff31117b100f2 Mon Sep 17 00:00:00 2001 From: Louis Wolmarans Date: Sun, 6 Jul 2025 22:13:32 +0200 Subject: [PATCH 034/318] wip --- .../flat-files/classes/class-snippet-files.php | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/php/flat-files/classes/class-snippet-files.php b/src/php/flat-files/classes/class-snippet-files.php index 093e33fd..8765780f 100644 --- a/src/php/flat-files/classes/class-snippet-files.php +++ b/src/php/flat-files/classes/class-snippet-files.php @@ -21,13 +21,15 @@ public function __construct( } public function register_hooks() { - add_action( 'code_snippets/create_snippet', [ $this, 'handle_snippet' ], 10, 2 ); - add_action( 'code_snippets/update_snippet', [ $this, 'handle_snippet' ], 10, 2 ); - add_action( 'code_snippets/delete_snippet', [ $this, 'delete_snippet' ], 10, 2 ); - add_action( 'code_snippets/activate_snippet', [ $this, 'activate_snippet' ], 10, 2 ); - add_action( 'code_snippets/deactivate_snippet', [ $this, 'deactivate_snippet' ], 10, 2 ); - - add_action( 'updated_option', [ $this, 'sync_active_shared_network_snippets' ], 10, 3 ); + if ( Settings\get_setting( 'general', 'enable_flat_files' ) ) { + add_action( 'code_snippets/create_snippet', [ $this, 'handle_snippet' ], 10, 2 ); + add_action( 'code_snippets/update_snippet', [ $this, 'handle_snippet' ], 10, 2 ); + add_action( 'code_snippets/delete_snippet', [ $this, 'delete_snippet' ], 10, 2 ); + add_action( 'code_snippets/activate_snippet', [ $this, 'activate_snippet' ], 10, 2 ); + add_action( 'code_snippets/deactivate_snippet', [ $this, 'deactivate_snippet' ], 10, 2 ); + + add_action( 'updated_option', [ $this, 'sync_active_shared_network_snippets' ], 10, 3 ); + } add_filter( 'code_snippets_settings_fields', [ $this, 'add_settings_fields' ], 10, 1 ); } From b983aa79f74cba15ecc33221815ea3155bee22c7 Mon Sep 17 00:00:00 2001 From: Louis Wolmarans Date: Sun, 6 Jul 2025 22:29:23 +0200 Subject: [PATCH 035/318] wip --- .../classes/class-file-system-adapter.php | 4 ++++ .../interfaces/interface-file-system.php | 1 + src/php/uninstall.php | 15 ++++++++++++++- 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/php/flat-files/classes/class-file-system-adapter.php b/src/php/flat-files/classes/class-file-system-adapter.php index c8085e5c..6c230f13 100644 --- a/src/php/flat-files/classes/class-file-system-adapter.php +++ b/src/php/flat-files/classes/class-file-system-adapter.php @@ -32,4 +32,8 @@ public function is_dir( string $path ): bool { public function mkdir( string $path, $chmod ) { return $this->fs->mkdir( $path, $chmod ); } + + public function rmdir( string $path, bool $recursive = false ): bool { + return $this->fs->rmdir( $path, $recursive ); + } } diff --git a/src/php/flat-files/interfaces/interface-file-system.php b/src/php/flat-files/interfaces/interface-file-system.php index 38ab5bd0..7560d7bb 100644 --- a/src/php/flat-files/interfaces/interface-file-system.php +++ b/src/php/flat-files/interfaces/interface-file-system.php @@ -8,4 +8,5 @@ public function exists( string $path ): bool; public function delete( string $path ): bool; public function is_dir( string $path ): bool; public function mkdir( string $path, $chmod ); + public function rmdir( string $path, bool $recursive = false ): bool; } diff --git a/src/php/uninstall.php b/src/php/uninstall.php index 4da58cfe..028aca5c 100644 --- a/src/php/uninstall.php +++ b/src/php/uninstall.php @@ -18,7 +18,7 @@ function complete_uninstall_enabled(): bool { $unified = false; if ( is_multisite() ) { - $menu_perms = get_site_option( 'menu_items', array() ); + $menu_perms = get_site_option( 'menu_items', [] ); $unified = empty( $menu_perms['snippets_settings'] ); } @@ -72,6 +72,17 @@ function uninstall_multisite() { delete_site_option( 'recently_activated_snippets' ); } +function delete_flat_files_directory() { + $flat_files_dir = WP_CONTENT_DIR . '/code-snippets'; + + if ( ! is_dir( $flat_files_dir ) ) { + return; + } + + $fs = new \Code_Snippets\WordPress_Filesystem_Adapter(); + $fs->rmdir( $flat_files_dir, true ); +} + /** * Uninstall the Code Snippets plugin. * @@ -85,5 +96,7 @@ function uninstall_plugin() { } else { uninstall_current_site(); } + + delete_flat_files_directory(); } } From e3a96ab1a469657e6fc392e264a7db351dbb4851 Mon Sep 17 00:00:00 2001 From: Louis Wolmarans Date: Sun, 6 Jul 2025 22:31:19 +0200 Subject: [PATCH 036/318] wip --- src/php/flat-files/classes/class-config-repository.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/php/flat-files/classes/class-config-repository.php b/src/php/flat-files/classes/class-config-repository.php index bcf48535..2087f572 100644 --- a/src/php/flat-files/classes/class-config-repository.php +++ b/src/php/flat-files/classes/class-config-repository.php @@ -3,6 +3,8 @@ class Snippet_Config_Repository implements Snippet_Config_Repository_Interface { + const CONFIG_FILE_NAME = 'index.php'; + private File_System_Interface $fs; public function __construct( File_System_Interface $fs ) { @@ -10,7 +12,7 @@ public function __construct( File_System_Interface $fs ) { } public function load( string $base_dir ): array { - $config_file_path = trailingslashit( $base_dir ) . 'index.php'; + $config_file_path = trailingslashit( $base_dir ) . static::CONFIG_FILE_NAME; return is_file( $config_file_path ) ? require $config_file_path @@ -18,7 +20,7 @@ public function load( string $base_dir ): array { } public function save( string $base_dir, array $active_snippets ): void { - $config_file_path = trailingslashit( $base_dir ) . 'index.php'; + $config_file_path = trailingslashit( $base_dir ) . static::CONFIG_FILE_NAME; $file_content = " Date: Sun, 6 Jul 2025 23:16:43 +0200 Subject: [PATCH 037/318] wip --- .../classes/class-snippet-files.php | 25 +++++++++++++++++++ src/php/settings/settings.php | 2 ++ 2 files changed, 27 insertions(+) diff --git a/src/php/flat-files/classes/class-snippet-files.php b/src/php/flat-files/classes/class-snippet-files.php index 8765780f..18a68176 100644 --- a/src/php/flat-files/classes/class-snippet-files.php +++ b/src/php/flat-files/classes/class-snippet-files.php @@ -32,6 +32,7 @@ public function register_hooks() { } add_filter( 'code_snippets_settings_fields', [ $this, 'add_settings_fields' ], 10, 1 ); + add_action( 'code_snippets/settings_updated', [ $this, 'create_all_flat_files' ], 10, 2 ); } public function handle_snippet( Snippet $snippet, string $table ) { @@ -206,4 +207,28 @@ public function add_settings_fields( array $fields ) { return $fields; } + + public function create_all_flat_files( array $settings, array $input ) { + if ( ! isset( $settings['general']['enable_flat_files'] ) ) { + return; + } + + if ( ! $settings['general']['enable_flat_files'] ) { + return; + } + + $db = code_snippets()->db; + $data = $db->fetch_active_snippets( Snippet::get_all_scopes() ); + + if ( empty( $data ) ) { + return; + } + + foreach ( $data as $table_name => $active_snippets ) { + foreach ( $active_snippets as $snippet ) { + $snippet_obj = get_snippet( $snippet['id'], $table_name === $db->ms_table ); + $this->handle_snippet( $snippet_obj, $table_name ); + } + } + } } diff --git a/src/php/settings/settings.php b/src/php/settings/settings.php index cf94f8b9..e3565b46 100644 --- a/src/php/settings/settings.php +++ b/src/php/settings/settings.php @@ -330,6 +330,8 @@ function sanitize_settings( array $input ): array { __( 'Settings saved.', 'code-snippets' ), 'updated' ); + + do_action( 'code_snippets/settings_updated', $settings, $input ); } return $settings; From 07f542bee02eda5b59fe8c4d2a71b1c3aa57bc1c Mon Sep 17 00:00:00 2001 From: Louis Wolmarans Date: Mon, 7 Jul 2025 22:04:13 +0200 Subject: [PATCH 038/318] wip --- .../classes/class-file-system-adapter.php | 8 ++++ .../classes/class-snippet-files.php | 44 +++++++++++-------- .../interfaces/interface-file-system.php | 2 + src/php/snippet-ops.php | 5 +-- 4 files changed, 37 insertions(+), 22 deletions(-) diff --git a/src/php/flat-files/classes/class-file-system-adapter.php b/src/php/flat-files/classes/class-file-system-adapter.php index 6c230f13..c17cef28 100644 --- a/src/php/flat-files/classes/class-file-system-adapter.php +++ b/src/php/flat-files/classes/class-file-system-adapter.php @@ -36,4 +36,12 @@ public function mkdir( string $path, $chmod ) { public function rmdir( string $path, bool $recursive = false ): bool { return $this->fs->rmdir( $path, $recursive ); } + + public function chmod( string $path, $chmod ): bool { + return $this->fs->chmod( $path, $chmod ); + } + + public function is_writable( string $path ): bool { + return $this->fs->is_writable( $path ); + } } diff --git a/src/php/flat-files/classes/class-snippet-files.php b/src/php/flat-files/classes/class-snippet-files.php index 18a68176..41f7b280 100644 --- a/src/php/flat-files/classes/class-snippet-files.php +++ b/src/php/flat-files/classes/class-snippet-files.php @@ -20,7 +20,11 @@ public function __construct( $this->config_repo = $config_repo; } - public function register_hooks() { + public function register_hooks(): void { + if ( ! $this->fs->is_writable( WP_CONTENT_DIR ) ) { + return; + } + if ( Settings\get_setting( 'general', 'enable_flat_files' ) ) { add_action( 'code_snippets/create_snippet', [ $this, 'handle_snippet' ], 10, 2 ); add_action( 'code_snippets/update_snippet', [ $this, 'handle_snippet' ], 10, 2 ); @@ -35,7 +39,7 @@ public function register_hooks() { add_action( 'code_snippets/settings_updated', [ $this, 'create_all_flat_files' ], 10, 2 ); } - public function handle_snippet( Snippet $snippet, string $table ) { + public function handle_snippet( Snippet $snippet, string $table ): void { $snippet_type = $snippet->get_type(); $handler = $this->handler_registry->get_handler( $snippet_type ); @@ -55,7 +59,7 @@ public function handle_snippet( Snippet $snippet, string $table ) { $this->config_repo->update( $base_dir, $snippet ); } - public function delete_snippet( Snippet $snippet, bool $network ) { + public function delete_snippet( Snippet $snippet, bool $network ): void { $snippet_type = $snippet->get_type(); $handler = $this->handler_registry->get_handler( $snippet_type ); @@ -72,7 +76,7 @@ public function delete_snippet( Snippet $snippet, bool $network ) { $this->config_repo->update( $base_dir, $snippet, true ); } - public function activate_snippet( Snippet $snippet, bool $network ) { + public function activate_snippet( Snippet $snippet, bool $network ): void { $snippet = get_snippet( $snippet->id, $network ); $snippet_type = $snippet->get_type(); $handler = $this->handler_registry->get_handler( $snippet_type ); @@ -95,7 +99,7 @@ public function activate_snippet( Snippet $snippet, bool $network ) { $this->config_repo->update( $base_dir, $snippet ); } - public function deactivate_snippet( int $snippet_id, bool $network ) { + public function deactivate_snippet( int $snippet_id, bool $network ): void { $snippet = get_snippet( $snippet_id, $network ); $snippet_type = $snippet->get_type(); $handler = $this->handler_registry->get_handler( $snippet_type ); @@ -110,7 +114,7 @@ public function deactivate_snippet( int $snippet_id, bool $network ) { $this->config_repo->update( $base_dir, $snippet ); } - public static function get_base_dir( string $table = '', string $snippet_type = '' ) { + public static function get_base_dir( string $table = '', string $snippet_type = '' ): string { $base_dir = WP_CONTENT_DIR . '/code-snippets'; if ( ! empty( $table ) ) { @@ -124,23 +128,27 @@ public static function get_base_dir( string $table = '', string $snippet_type = return $base_dir; } - private function maybe_create_directory( string $dir ) { + private function maybe_create_directory( string $dir ): void { if ( ! $this->fs->is_dir( $dir ) ) { - $this->fs->mkdir( $dir, FS_CHMOD_DIR ); + $result = wp_mkdir_p( $dir ); + + if ( $result ) { + $this->fs->chmod( $dir, FS_CHMOD_DIR ); + } } } - private function get_snippet_file_path( string $base_dir, int $snippet_id, string $ext ) { + private function get_snippet_file_path( string $base_dir, int $snippet_id, string $ext ): string { return trailingslashit( $base_dir ) . $snippet_id . '.' . $ext; } - private function delete_file( string $file_path ) { + private function delete_file( string $file_path ): void { if ( $this->fs->exists( $file_path ) ) { $this->fs->delete( $file_path ); } } - public function sync_active_shared_network_snippets( $option, $old_value, $value ) { + public function sync_active_shared_network_snippets( $option, $old_value, $value ): void { if ( 'active_shared_network_snippets' !== $option ) { return; } @@ -156,7 +164,7 @@ public function sync_active_shared_network_snippets( $option, $old_value, $value $this->fs->put_contents( $file_path, $file_content, FS_CHMOD_FILE ); } - public static function get_active_snippets_from_flat_files() { + public static function get_active_snippets_from_flat_files( array $scopes = [] ): array { $snippets = []; $table = code_snippets()->db->get_table_name(); @@ -168,8 +176,8 @@ public static function get_active_snippets_from_flat_files() { $snippets[ $table ] = array_filter( $site_snippets, - function ( $snippet ) { - return $snippet['active']; + function ( $snippet ) use ( $scopes ) { + return $snippet['active'] && in_array( $snippet['scope'], $scopes, true ); } ); } @@ -188,8 +196,8 @@ function ( $snippet ) { $snippets[ $ms_table ] = array_filter( $ms_snippets, - function ( $snippet ) use ( $active_shared_ids ) { - return $snippet['active'] || in_array( intval( $snippet['id'] ), $active_shared_ids, true ); + function ( $snippet ) use ( $active_shared_ids, $scopes ) { + return ( $snippet['active'] || in_array( intval( $snippet['id'] ), $active_shared_ids, true ) ) && in_array( $snippet['scope'], $scopes, true ); } ); } @@ -198,7 +206,7 @@ function ( $snippet ) use ( $active_shared_ids ) { return $snippets; } - public function add_settings_fields( array $fields ) { + public function add_settings_fields( array $fields ): array { $fields['general']['enable_flat_files'] = [ 'name' => __( 'Enable Flat Files', 'code-snippets' ), 'type' => 'checkbox', @@ -208,7 +216,7 @@ public function add_settings_fields( array $fields ) { return $fields; } - public function create_all_flat_files( array $settings, array $input ) { + public function create_all_flat_files( array $settings, array $input ): void { if ( ! isset( $settings['general']['enable_flat_files'] ) ) { return; } diff --git a/src/php/flat-files/interfaces/interface-file-system.php b/src/php/flat-files/interfaces/interface-file-system.php index 7560d7bb..71b0efe2 100644 --- a/src/php/flat-files/interfaces/interface-file-system.php +++ b/src/php/flat-files/interfaces/interface-file-system.php @@ -9,4 +9,6 @@ public function delete( string $path ): bool; public function is_dir( string $path ): bool; public function mkdir( string $path, $chmod ); public function rmdir( string $path, bool $recursive = false ): bool; + public function chmod( string $path, $chmod ): bool; + public function is_writable( string $path ): bool; } diff --git a/src/php/snippet-ops.php b/src/php/snippet-ops.php index 8fc90ff2..9cf86b91 100644 --- a/src/php/snippet-ops.php +++ b/src/php/snippet-ops.php @@ -686,7 +686,7 @@ function execute_active_snippets_from_flat_files(): bool { $db = code_snippets()->db; $scopes = [ 'global', 'single-use', is_admin() ? 'admin' : 'front-end' ]; - $data = Snippet_Files::get_active_snippets_from_flat_files(); + $data = Snippet_Files::get_active_snippets_from_flat_files( $scopes ); // Detect if a snippet is currently being edited, and if so, spare it from execution. $edit_id = 0; @@ -713,9 +713,6 @@ function execute_active_snippets_from_flat_files(): bool { // Loop through the returned snippets and execute the PHP code. foreach ( $active_snippets as $snippet ) { - if ( ! in_array( $snippet['scope'], $scopes, true ) ) { - continue; - } $snippet_id = intval( $snippet['id'] ); From 19708267fe80d473b9bed8a51e860bc55c1dc534 Mon Sep 17 00:00:00 2001 From: Louis Wolmarans Date: Mon, 7 Jul 2025 22:15:22 +0200 Subject: [PATCH 039/318] wip --- .../flat-files/classes/class-snippet-files.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/php/flat-files/classes/class-snippet-files.php b/src/php/flat-files/classes/class-snippet-files.php index 41f7b280..0f98fa57 100644 --- a/src/php/flat-files/classes/class-snippet-files.php +++ b/src/php/flat-files/classes/class-snippet-files.php @@ -153,6 +153,10 @@ public function sync_active_shared_network_snippets( $option, $old_value, $value return; } + $this->create_active_shared_network_snippets_file( $value ); + } + + private function create_active_shared_network_snippets_file( $value ): void { $table = code_snippets()->db->get_table_name(); $base_dir = self::get_base_dir( $table ); @@ -225,6 +229,11 @@ public function create_all_flat_files( array $settings, array $input ): void { return; } + $this->create_snippet_flat_files(); + $this->create_active_shared_network_snippets_config_file(); + } + + private function create_snippet_flat_files(): void { $db = code_snippets()->db; $data = $db->fetch_active_snippets( Snippet::get_all_scopes() ); @@ -239,4 +248,11 @@ public function create_all_flat_files( array $settings, array $input ): void { } } } + + private function create_active_shared_network_snippets_config_file(): void { + $active_shared_network_snippets = get_option( 'active_shared_network_snippets' ); + if ( false !== $active_shared_network_snippets ) { + $this->create_active_shared_network_snippets_file( $active_shared_network_snippets ); + } + } } From 4cd5ee80783841433c9dc2c1b2a942b546c821ef Mon Sep 17 00:00:00 2001 From: Louis Wolmarans Date: Mon, 7 Jul 2025 22:24:41 +0200 Subject: [PATCH 040/318] wip --- src/php/snippet-ops.php | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/php/snippet-ops.php b/src/php/snippet-ops.php index 9cf86b91..9bdb7741 100644 --- a/src/php/snippet-ops.php +++ b/src/php/snippet-ops.php @@ -713,8 +713,8 @@ function execute_active_snippets_from_flat_files(): bool { // Loop through the returned snippets and execute the PHP code. foreach ( $active_snippets as $snippet ) { - $snippet_id = intval( $snippet['id'] ); + $code = $snippet['code']; // If the snippet is a single-use snippet, deactivate it before execution to ensure that the process always happens. if ( 'single-use' === $snippet['scope'] ) { @@ -743,7 +743,7 @@ function execute_active_snippets_from_flat_files(): bool { if ( apply_filters( 'code_snippets/allow_execute_snippet', true, $snippet_id, $table_name ) && ! ( $edit_id === $snippet_id && $table_name === $edit_table ) ) { $file = $base_dir . '/' . $snippet_id . '.php'; - execute_snippet_from_flat_file( $file, $snippet_id ); + execute_snippet_from_flat_file( $code, $file, $snippet_id ); } } } @@ -759,8 +759,12 @@ function cs_sort_snippets_by_priority( array $snippets ): array { return $snippets; } -function execute_snippet_from_flat_file( $file, int $id = 0, bool $force = false ) { - if ( ! is_file( $file ) || ( ! $force && defined( 'CODE_SNIPPETS_SAFE_MODE' ) && CODE_SNIPPETS_SAFE_MODE ) ) { +function execute_snippet_from_flat_file( $code, $file, int $id = 0, bool $force = false ) { + if ( ! is_file( $file ) ) { + execute_snippet( $code, $id, $force ); + } + + if ( ! $force && defined( 'CODE_SNIPPETS_SAFE_MODE' ) && CODE_SNIPPETS_SAFE_MODE ) { return false; } From 868ee9e8792b2df5230792ed66d0b9a33e185a8a Mon Sep 17 00:00:00 2001 From: Shea Bunge Date: Thu, 24 Jul 2025 13:03:09 +1000 Subject: [PATCH 041/318] Bootstrap new admin menu for snippets table uplift. --- config/webpack-js.ts | 3 +- package-lock.json | 4 +- package.json | 2 +- src/code-snippets.php | 6 +- src/composer.lock | 10 +- src/css/manage-legacy.scss | 213 ++++++++++++++++ src/css/{manage => manage-legacy}/_cloud.scss | 0 src/css/manage.scss | 213 ---------------- .../components/SnippetForm/page/Notices.tsx | 2 +- .../SnippetsTable/SnippetsTable.tsx | 4 + src/js/components/SnippetsTable/index.ts | 1 + .../{ => common}/DismissableNotice.tsx | 0 src/js/edit.ts | 4 + src/js/edit.tsx | 12 - src/js/hooks/useRestAPI.tsx | 6 +- src/js/hooks/useSnippetForm.tsx | 2 +- src/js/hooks/useSnippetsList.tsx | 2 +- src/js/manage-legacy.ts | 5 + src/js/manage.ts | 7 +- src/js/types/Snippet.ts | 3 - src/js/utils/bootstrap.tsx | 34 +++ src/js/utils/hooks.ts | 21 -- src/js/utils/snippets.ts | 10 +- .../admin-menus/class-manage-menu-legacy.php | 231 ++++++++++++++++++ src/php/admin-menus/class-manage-menu.php | 168 +++---------- src/php/class-admin.php | 3 +- src/php/class-plugin.php | 12 +- src/php/views/manage.php | 2 +- src/php/views/partials/cloud-search.php | 2 +- src/php/views/partials/list-table-notices.php | 2 +- src/php/views/partials/list-table.php | 2 +- src/readme.txt | 2 +- 32 files changed, 570 insertions(+), 418 deletions(-) create mode 100644 src/css/manage-legacy.scss rename src/css/{manage => manage-legacy}/_cloud.scss (100%) create mode 100644 src/js/components/SnippetsTable/SnippetsTable.tsx create mode 100644 src/js/components/SnippetsTable/index.ts rename src/js/components/{ => common}/DismissableNotice.tsx (100%) create mode 100644 src/js/edit.ts delete mode 100644 src/js/edit.tsx create mode 100644 src/js/manage-legacy.ts create mode 100644 src/js/utils/bootstrap.tsx delete mode 100644 src/js/utils/hooks.ts create mode 100644 src/php/admin-menus/class-manage-menu-legacy.php diff --git a/config/webpack-js.ts b/config/webpack-js.ts index b8a0d1fb..c46f80b9 100644 --- a/config/webpack-js.ts +++ b/config/webpack-js.ts @@ -24,9 +24,10 @@ const babelConfig = { export const jsWebpackConfig: Configuration = { entry: { - edit: { import: `${SOURCE_DIR}/edit.tsx`, dependOn: 'editor' }, + edit: { import: `${SOURCE_DIR}/edit.ts`, dependOn: 'editor' }, editor: `${SOURCE_DIR}/editor.ts`, manage: `${SOURCE_DIR}/manage.ts`, + 'manage-legacy': `${SOURCE_DIR}/manage-legacy.ts`, mce: `${SOURCE_DIR}/mce.ts`, prism: `${SOURCE_DIR}/prism.ts`, settings: { import: `${SOURCE_DIR}/settings.ts`, dependOn: 'editor' } diff --git a/package-lock.json b/package-lock.json index 0ff4790b..f8ca4bee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "code-snippets", - "version": "3.7.0-beta.1", + "version": "3.8.0-dev.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "code-snippets", - "version": "3.7.0-beta.1", + "version": "3.8.0-dev.1", "license": "GPL-2.0-or-later", "dependencies": { "@codemirror/fold": "^0.19.4", diff --git a/package.json b/package.json index b13552b4..6c04dc79 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "code-snippets", "description": "Manage code snippets running on a WordPress-powered site through a graphical interface.", "homepage": "https://codesnippets.pro", - "version": "3.7.0-beta.1", + "version": "3.8.0-dev.1", "main": "src/dist/edit.js", "directories": { "test": "tests" diff --git a/src/code-snippets.php b/src/code-snippets.php index 70841f72..3eac6ba8 100644 --- a/src/code-snippets.php +++ b/src/code-snippets.php @@ -8,11 +8,11 @@ * License: GPL-2.0-or-later * License URI: license.txt * Text Domain: code-snippets - * Version: 3.7.0-beta.1 + * Version: 3.8.0-dev.1 * Requires PHP: 7.4 * Requires at least: 5.0 * - * @version 3.7.0-beta.1 + * @version 3.8.0-dev.1 * @package Code_Snippets * @author Shea Bunge * @copyright 2012-2024 Code Snippets Pro @@ -37,7 +37,7 @@ * * @const string */ - define( 'CODE_SNIPPETS_VERSION', '3.7.0-beta.1' ); + define( 'CODE_SNIPPETS_VERSION', '3.8.0-dev.1' ); /** * The full path to the main file of this plugin. diff --git a/src/composer.lock b/src/composer.lock index b256075a..3113a000 100644 --- a/src/composer.lock +++ b/src/composer.lock @@ -156,16 +156,16 @@ "packages-dev": [ { "name": "dealerdirect/phpcodesniffer-composer-installer", - "version": "v1.1.1", + "version": "v1.1.2", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/composer-installer.git", - "reference": "6e0fa428497bf560152ee73ffbb8af5c6a56b0dd" + "reference": "e9cf5e4bbf7eeaf9ef5db34938942602838fc2b1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/6e0fa428497bf560152ee73ffbb8af5c6a56b0dd", - "reference": "6e0fa428497bf560152ee73ffbb8af5c6a56b0dd", + "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/e9cf5e4bbf7eeaf9ef5db34938942602838fc2b1", + "reference": "e9cf5e4bbf7eeaf9ef5db34938942602838fc2b1", "shasum": "" }, "require": { @@ -248,7 +248,7 @@ "type": "thanks_dev" } ], - "time": "2025-06-27T17:24:01+00:00" + "time": "2025-07-17T20:45:56+00:00" }, { "name": "phpcompatibility/php-compatibility", diff --git a/src/css/manage-legacy.scss b/src/css/manage-legacy.scss new file mode 100644 index 00000000..d7a9972c --- /dev/null +++ b/src/css/manage-legacy.scss @@ -0,0 +1,213 @@ +/** + * Custom styling for the snippets table + */ + +@use 'sass:map'; +@use 'sass:color'; +@use 'common/theme'; +@use 'common/badges'; +@use 'common/switch'; +@use 'common/select'; +@use 'manage-legacy/cloud'; + +.column-name, +.column-type { + .dashicons { + font-size: 16px; + inline-size: 16px; + block-size: 16px; + vertical-align: middle; + } + + .dashicons-clock { + vertical-align: middle; + } +} + +.active-snippet .column-name > a { + font-weight: 600; +} + +.active-snippet { + td, th { + background-color: rgba(#78c8e6, 0.06); + } + + th.check-column { + border-inline-start: 2px solid #2ea2cc; + } +} + +.column-priority input { + appearance: none; + background: none; + border: none; + box-shadow: none; + inline-size: 4em; + color: #666; + text-align: center; + + &:hover, &:focus, &:active { + color: #000; + background-color: #f5f5f5; + background-color: rgb(0 0 0 / 10%); + border-radius: 6px; + } + + &:disabled { + color: inherit; + } +} + +.clear-filters { + vertical-align: baseline !important; +} + +.snippets { + td.column-id { + text-align: center; + } + + tr { + background: #fff; + } + + ol, ul { + margin: 0 0 1.5em 1.5em; + } + + ul { + list-style: disc; + } + + th.sortable a, th.sorted a { + display: flex; + flex-direction: row; + } + + .row-actions { + color: #ddd; + position: relative; + inset-inline-start: 0; + } + + .column-activate { + padding-inline-end: 0 !important; + } + + .clear-filters { + vertical-align: middle; + } + + tfoot th.check-column { + padding: 13px 0 0 3px; + } + + thead th.check-column, + tfoot th.check-column, + .inactive-snippet th.check-column { + padding-inline-start: 5px; + } + + .active-snippet, .inactive-snippet { + td, th { + padding: 10px 9px; + border: none; + box-shadow: inset 0 -1px 0 rgb(0 0 0 / 10%); + } + } + + tr.active-snippet + tr.inactive-snippet th, + tr.active-snippet + tr.inactive-snippet td { + border-block-start: 1px solid rgb(0 0 0 / 3%); + box-shadow: inset 0 1px 0 rgb(0 0 0 / 2%), inset 0 -1px 0 #e1e1e1; + } + + &, #all-snippets-table, #search-snippets-table { + a.delete:hover { + border-block-end: 1px solid #f00; + color: #f00; + } + } + + #wpbody-content & .column-name { + white-space: nowrap; /* prevents wrapping of snippet title */ + } +} + +td.column-description { + max-inline-size: 700px; + + pre { + white-space: unset; + } +} + +.inactive-snippet { + @include theme.link-colors(#579); +} + +@media screen and (width <= 782px) { + p.search-box { + float: inline-start; + position: initial; + margin: 1em 0 0; + block-size: auto; + } +} + +.wp-list-table .is-expanded td.column-activate.activate { + /* fix for mobile layout */ + display: table-cell !important; +} + +.nav-tab-wrapper + .subsubsub, p.search-box { + margin: 10px 0 0; +} + +.snippet-type-description { + border-block-end: 1px solid #ccc; + margin: 0; + padding: 1em 0; +} + +.code-snippets-notice a.notice-dismiss { + text-decoration: none; +} + +.refresh-button-container { + display: flex; + align-items: center; + justify-content: flex-start; + margin-block: 15px -39px; + gap: 7px; +} + +#refresh-button { + inline-size: 30px; + padding: 0; + font-size: 20px; + line-height: 1.4; +} + +.wrap h2.nav-tab-wrapper { + display: flex; + flex-flow: row wrap-reverse; + gap: 0.5em; + + .nav-tab { + display: flex; + flex-flow: row wrap; + align-items: center; + gap: 8px; + margin: 0; + } +} + +@media screen and (width <= 1190px) { + .nav-tab { + .snippet-label { + display: none; + } + } +} diff --git a/src/css/manage/_cloud.scss b/src/css/manage-legacy/_cloud.scss similarity index 100% rename from src/css/manage/_cloud.scss rename to src/css/manage-legacy/_cloud.scss diff --git a/src/css/manage.scss b/src/css/manage.scss index 82a88322..e69de29b 100644 --- a/src/css/manage.scss +++ b/src/css/manage.scss @@ -1,213 +0,0 @@ -/** - * Custom styling for the snippets table - */ - -@use 'sass:map'; -@use 'sass:color'; -@use 'common/theme'; -@use 'common/badges'; -@use 'common/switch'; -@use 'common/select'; -@use 'manage/cloud'; - -.column-name, -.column-type { - .dashicons { - font-size: 16px; - inline-size: 16px; - block-size: 16px; - vertical-align: middle; - } - - .dashicons-clock { - vertical-align: middle; - } -} - -.active-snippet .column-name > a { - font-weight: 600; -} - -.active-snippet { - td, th { - background-color: rgba(#78c8e6, 0.06); - } - - th.check-column { - border-inline-start: 2px solid #2ea2cc; - } -} - -.column-priority input { - appearance: none; - background: none; - border: none; - box-shadow: none; - inline-size: 4em; - color: #666; - text-align: center; - - &:hover, &:focus, &:active { - color: #000; - background-color: #f5f5f5; - background-color: rgb(0 0 0 / 10%); - border-radius: 6px; - } - - &:disabled { - color: inherit; - } -} - -.clear-filters { - vertical-align: baseline !important; -} - -.snippets { - td.column-id { - text-align: center; - } - - tr { - background: #fff; - } - - ol, ul { - margin: 0 0 1.5em 1.5em; - } - - ul { - list-style: disc; - } - - th.sortable a, th.sorted a { - display: flex; - flex-direction: row; - } - - .row-actions { - color: #ddd; - position: relative; - inset-inline-start: 0; - } - - .column-activate { - padding-inline-end: 0 !important; - } - - .clear-filters { - vertical-align: middle; - } - - tfoot th.check-column { - padding: 13px 0 0 3px; - } - - thead th.check-column, - tfoot th.check-column, - .inactive-snippet th.check-column { - padding-inline-start: 5px; - } - - .active-snippet, .inactive-snippet { - td, th { - padding: 10px 9px; - border: none; - box-shadow: inset 0 -1px 0 rgb(0 0 0 / 10%); - } - } - - tr.active-snippet + tr.inactive-snippet th, - tr.active-snippet + tr.inactive-snippet td { - border-block-start: 1px solid rgb(0 0 0 / 3%); - box-shadow: inset 0 1px 0 rgb(0 0 0 / 2%), inset 0 -1px 0 #e1e1e1; - } - - &, #all-snippets-table, #search-snippets-table { - a.delete:hover { - border-block-end: 1px solid #f00; - color: #f00; - } - } - - #wpbody-content & .column-name { - white-space: nowrap; /* prevents wrapping of snippet title */ - } -} - -td.column-description { - max-inline-size: 700px; - - pre { - white-space: unset; - } -} - -.inactive-snippet { - @include theme.link-colors(#579); -} - -@media screen and (width <= 782px) { - p.search-box { - float: inline-start; - position: initial; - margin: 1em 0 0; - block-size: auto; - } -} - -.wp-list-table .is-expanded td.column-activate.activate { - /* fix for mobile layout */ - display: table-cell !important; -} - -.nav-tab-wrapper + .subsubsub, p.search-box { - margin: 10px 0 0; -} - -.snippet-type-description { - border-block-end: 1px solid #ccc; - margin: 0; - padding: 1em 0; -} - -.code-snippets-notice a.notice-dismiss { - text-decoration: none; -} - -.refresh-button-container { - display: flex; - align-items: center; - justify-content: flex-start; - margin-block: 15px -39px; - gap: 7px; -} - -#refresh-button { - inline-size: 30px; - padding: 0; - font-size: 20px; - line-height: 1.4; -} - -.wrap h2.nav-tab-wrapper { - display: flex; - flex-flow: row wrap-reverse; - gap: 0.5em; - - .nav-tab { - display: flex; - flex-flow: row wrap; - align-items: center; - gap: 8px; - margin: 0; - } -} - -@media screen and (width <= 1190px) { - .nav-tab { - .snippet-label { - display: none; - } - } -} diff --git a/src/js/components/SnippetForm/page/Notices.tsx b/src/js/components/SnippetForm/page/Notices.tsx index 7b8523f4..5773752b 100644 --- a/src/js/components/SnippetForm/page/Notices.tsx +++ b/src/js/components/SnippetForm/page/Notices.tsx @@ -1,7 +1,7 @@ import React from 'react' import { __, sprintf } from '@wordpress/i18n' import { useSnippetForm } from '../../../hooks/useSnippetForm' -import { DismissibleNotice } from '../../DismissableNotice' +import { DismissibleNotice } from '../../common/DismissableNotice' export const Notices: React.FC = () => { const { currentNotice, setCurrentNotice, snippet, setSnippet } = useSnippetForm() diff --git a/src/js/components/SnippetsTable/SnippetsTable.tsx b/src/js/components/SnippetsTable/SnippetsTable.tsx new file mode 100644 index 00000000..71c97d6d --- /dev/null +++ b/src/js/components/SnippetsTable/SnippetsTable.tsx @@ -0,0 +1,4 @@ +import React from "react" + +export const SnippetsTable: React.FC = () => +
TODO
diff --git a/src/js/components/SnippetsTable/index.ts b/src/js/components/SnippetsTable/index.ts new file mode 100644 index 00000000..b8db6e57 --- /dev/null +++ b/src/js/components/SnippetsTable/index.ts @@ -0,0 +1 @@ +export * from './SnippetsTable' diff --git a/src/js/components/DismissableNotice.tsx b/src/js/components/common/DismissableNotice.tsx similarity index 100% rename from src/js/components/DismissableNotice.tsx rename to src/js/components/common/DismissableNotice.tsx diff --git a/src/js/edit.ts b/src/js/edit.ts new file mode 100644 index 00000000..0bbf0d2f --- /dev/null +++ b/src/js/edit.ts @@ -0,0 +1,4 @@ +import { SnippetForm } from './components/SnippetForm' +import { loadComponent } from './utils/bootstrap' + +loadComponent('edit-snippet-form-container', SnippetForm) diff --git a/src/js/edit.tsx b/src/js/edit.tsx deleted file mode 100644 index 8968cc23..00000000 --- a/src/js/edit.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react' -import { createRoot } from 'react-dom/client' -import { SnippetForm } from './components/SnippetForm' - -const container = document.getElementById('edit-snippet-form-container') - -if (container) { - const root = createRoot(container) - root.render() -} else { - console.error('Could not find snippet edit form container.') -} diff --git a/src/js/hooks/useRestAPI.tsx b/src/js/hooks/useRestAPI.tsx index fc79de06..4e945095 100644 --- a/src/js/hooks/useRestAPI.tsx +++ b/src/js/hooks/useRestAPI.tsx @@ -1,6 +1,6 @@ import React, { useMemo } from 'react' import axios from 'axios' -import { createContextHook } from '../utils/hooks' +import { createContextHook } from '../utils/bootstrap' import { REST_API_AXIOS_CONFIG } from '../utils/restAPI' import { buildSnippetsAPI } from '../utils/snippets/api' import type { SnippetsAPI } from '../utils/snippets/api' @@ -36,13 +36,13 @@ const buildRestAPI = (axiosInstance: AxiosInstance): RestAPI => ({ get: (url: string): Promise => debugRequest('GET', url, axiosInstance.get, never>(url)), - post: (url: string, data?: object): Promise => + post: (url: string, data?: D): Promise => debugRequest('POST', url, axiosInstance.post, typeof data>(url, data), data), del: (url: string): Promise => debugRequest('DELETE', url, axiosInstance.delete, never>(url)), - put: (url: string, data?: object): Promise => + put: (url: string, data?: D): Promise => debugRequest('PUT', url, axiosInstance.put, typeof data>(url, data), data) }) diff --git a/src/js/hooks/useSnippetForm.tsx b/src/js/hooks/useSnippetForm.tsx index 458b9f39..645d5a0f 100644 --- a/src/js/hooks/useSnippetForm.tsx +++ b/src/js/hooks/useSnippetForm.tsx @@ -1,6 +1,6 @@ import { isAxiosError } from 'axios' import React, { useCallback, useMemo, useState } from 'react' -import { createContextHook } from '../utils/hooks' +import { createContextHook } from '../utils/bootstrap' import { isLicensed } from '../utils/screen' import { isProSnippet } from '../utils/snippets/snippets' import type { Dispatch, PropsWithChildren, SetStateAction } from 'react' diff --git a/src/js/hooks/useSnippetsList.tsx b/src/js/hooks/useSnippetsList.tsx index 06325cc4..f65c6566 100644 --- a/src/js/hooks/useSnippetsList.tsx +++ b/src/js/hooks/useSnippetsList.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useEffect, useState } from 'react' -import { createContextHook } from '../utils/hooks' +import { createContextHook } from '../utils/bootstrap' import { isNetworkAdmin } from '../utils/screen' import { useRestAPI } from './useRestAPI' import type { PropsWithChildren } from 'react' diff --git a/src/js/manage-legacy.ts b/src/js/manage-legacy.ts new file mode 100644 index 00000000..634cad9b --- /dev/null +++ b/src/js/manage-legacy.ts @@ -0,0 +1,5 @@ +import { handleShowCloudPreview, handleSnippetActivationSwitches, handleSnippetPriorityChanges } from './services/manage' + +handleSnippetActivationSwitches() +handleSnippetPriorityChanges() +handleShowCloudPreview() diff --git a/src/js/manage.ts b/src/js/manage.ts index 634cad9b..e75a9281 100644 --- a/src/js/manage.ts +++ b/src/js/manage.ts @@ -1,5 +1,4 @@ -import { handleShowCloudPreview, handleSnippetActivationSwitches, handleSnippetPriorityChanges } from './services/manage' +import { SnippetsTable } from './components/SnippetsTable' +import { loadComponent } from './utils/bootstrap' -handleSnippetActivationSwitches() -handleSnippetPriorityChanges() -handleShowCloudPreview() +loadComponent('snippets-table-container', SnippetsTable) diff --git a/src/js/types/Snippet.ts b/src/js/types/Snippet.ts index 20761992..dfdec93f 100644 --- a/src/js/types/Snippet.ts +++ b/src/js/types/Snippet.ts @@ -1,5 +1,3 @@ -import type { ConditionGroups } from './ConditionGroups' - export interface Snippet { readonly id: number readonly name: string @@ -14,7 +12,6 @@ export interface Snippet { readonly modified?: string readonly conditionId: number readonly code_error?: readonly [string, number] | null - readonly conditions: ConditionGroups } export type SnippetCodeType = 'php' | 'html' | 'css' | 'js' diff --git a/src/js/utils/bootstrap.tsx b/src/js/utils/bootstrap.tsx new file mode 100644 index 00000000..978a7429 --- /dev/null +++ b/src/js/utils/bootstrap.tsx @@ -0,0 +1,34 @@ +import { createContext, useContext } from 'react' +import { createRoot } from 'react-dom/client' +import type { Context, FunctionComponent } from 'react' +import React from 'react' + +export const loadComponent = (containerId: string, Component: FunctionComponent): void => { + const container = document.getElementById('snippets-table-container') + + if (container) { + const root = createRoot(container) + root.render() + } else { + console.error(`Could not find ${containerId.replace(/-_/, ' ')}.`) + } +} + +export const createContextHook = (name: string): [ + Context, + () => T +] => { + const contextValue = createContext(undefined) + + const useContextHook = (): T => { + const value = useContext(contextValue) + + if (value === undefined) { + throw Error(`use${name} can only be used within a ${name} context provider.`) + } + + return value + } + + return [contextValue, useContextHook] +} diff --git a/src/js/utils/hooks.ts b/src/js/utils/hooks.ts deleted file mode 100644 index 3a52d8c2..00000000 --- a/src/js/utils/hooks.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { createContext, useContext } from 'react' -import type { Context } from 'react' - -export const createContextHook = (name: string): [ - Context, - () => T -] => { - const contextValue = createContext(undefined) - - const useContextHook = (): T => { - const value = useContext(contextValue) - - if (value === undefined) { - throw Error(`use${name} can only be used within a ${name} context provider.`) - } - - return value - } - - return [contextValue, useContextHook] -} diff --git a/src/js/utils/snippets.ts b/src/js/utils/snippets.ts index 123d826c..63c167f3 100644 --- a/src/js/utils/snippets.ts +++ b/src/js/utils/snippets.ts @@ -14,7 +14,8 @@ const defaults: Omit = { active: false, network: isNetworkAdmin(), shared_network: null, - priority: 10 + priority: 10, + conditionId: 0 } const isAbsInt = (value: unknown): value is number => @@ -44,10 +45,13 @@ export const createSnippetObject = (fields: unknown = undefined): Snippet => { scope: 'scope' in fields && isValidScope(fields.scope) ? fields.scope : defaults.scope, modified: 'modified' in fields && 'string' === typeof fields.modified ? fields.modified : defaults.modified, active: 'active' in fields && 'boolean' === typeof fields.active ? fields.active : defaults.active, - network: 'network' in fields && isBooleanOrUndefined(fields.network) ? fields.network : defaults.network, + network: 'network' in fields && 'boolean' === typeof fields.network ? fields.network : defaults.network, shared_network: 'shared_network' in fields && isBooleanOrUndefined(fields.shared_network) ? fields.shared_network : defaults.shared_network, - priority: 'priority' in fields && 'number' === typeof fields.priority ? fields.priority : defaults.priority + priority: 'priority' in fields && 'number' === typeof fields.priority ? fields.priority : defaults.priority, + conditionId: 'conditionId' in fields && isAbsInt(fields.conditionId) ? fields.conditionId + : 'condition_id' in fields && isAbsInt(fields.condition_id) ? fields.condition_id + : defaults.conditionId } } diff --git a/src/php/admin-menus/class-manage-menu-legacy.php b/src/php/admin-menus/class-manage-menu-legacy.php new file mode 100644 index 00000000..dd3a688f --- /dev/null +++ b/src/php/admin-menus/class-manage-menu-legacy.php @@ -0,0 +1,231 @@ +load(); + + $this->cloud_search_list_table = new Cloud_Search_List_Table(); + $this->cloud_search_list_table->prepare_items(); + + $this->list_table = new List_Table(); + $this->list_table->prepare_items(); + } + + /** + * Render the menu + */ + public function render() { + $this->render_view( 'manage' ); + } + + /** + * Enqueue scripts and stylesheets for the admin page. + */ + public function enqueue_assets() { + $plugin = code_snippets(); + + wp_enqueue_style( + 'code-snippets-manage-legacy', + plugins_url( 'dist/manage-legacy.css', $plugin->file ), + [], + $plugin->version + ); + + wp_enqueue_script( + 'code-snippets-manage-legacy-js', + plugins_url( 'dist/manage-legacy.js', $plugin->file ), + [ 'wp-i18n' ], + $plugin->version, + true + ); + + wp_set_script_translations( 'code-snippets-manage-legacy-js', 'code-snippets' ); + + if ( 'cloud' === $this->get_current_type() || 'cloud_search' === $this->get_current_type() ) { + Front_End::enqueue_all_prism_themes(); + } + } + + /** + * Get the currently displayed snippet type. + * + * @return string + */ + protected function get_current_type(): string { + $types = Plugin::get_types(); + $current_type = isset( $_GET['type'] ) ? sanitize_key( wp_unslash( $_GET['type'] ) ) : 'all'; + return isset( $types[ $current_type ] ) ? $current_type : 'all'; + } + + /** + * Print the status and error messages + * + * @return void + */ + protected function print_messages() { + $this->render_view( 'partials/list-table-notices' ); + } + + /** + * Handles saving the user's snippets per page preference + * + * @param mixed $status Current screen option status. + * @param string $option The screen option name. + * @param mixed $value Screen option value. + * + * @return mixed + */ + public function save_screen_option( $status, string $option, $value ) { + return 'snippets_per_page' === $option ? $value : $status; + } + + /** + * Update the priority value for a snippet. + * + * @param Snippet $snippet Snippet to update. + * + * @return void + */ + private function update_snippet_priority( Snippet $snippet ) { + global $wpdb; + $table = code_snippets()->db->get_table_name( $snippet->network ); + + $wpdb->update( + $table, + array( 'priority' => $snippet->priority ), + array( 'id' => $snippet->id ), + array( '%d' ), + array( '%d' ) + ); + + clean_snippets_cache( $table ); + } + + /** + * Handle AJAX requests + */ + public function ajax_callback() { + check_ajax_referer( 'code_snippets_manage_ajax' ); + + if ( ! isset( $_POST['field'], $_POST['snippet'] ) ) { + wp_send_json_error( + array( + 'type' => 'param_error', + 'message' => 'incomplete request', + ) + ); + } + + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + $snippet_data = array_map( 'sanitize_text_field', json_decode( wp_unslash( $_POST['snippet'] ), true ) ); + + $snippet = new Snippet( $snippet_data ); + $field = sanitize_key( $_POST['field'] ); + + if ( 'priority' === $field ) { + + if ( ! isset( $snippet_data['priority'] ) || ! is_numeric( $snippet_data['priority'] ) ) { + wp_send_json_error( + array( + 'type' => 'param_error', + 'message' => 'missing snippet priority data', + ) + ); + } + + $this->update_snippet_priority( $snippet ); + + } elseif ( 'active' === $field ) { + + if ( ! isset( $snippet_data['active'] ) ) { + wp_send_json_error( + array( + 'type' => 'param_error', + 'message' => 'missing snippet active data', + ) + ); + } + + if ( $snippet->shared_network ) { + $active_shared_snippets = get_option( 'active_shared_network_snippets', array() ); + + if ( in_array( $snippet->id, $active_shared_snippets, true ) !== $snippet->active ) { + + $active_shared_snippets = $snippet->active ? + array_merge( $active_shared_snippets, array( $snippet->id ) ) : + array_diff( $active_shared_snippets, array( $snippet->id ) ); + + update_option( 'active_shared_network_snippets', $active_shared_snippets ); + clean_active_snippets_cache( code_snippets()->db->ms_table ); + } + } elseif ( $snippet->active ) { + $result = activate_snippet( $snippet->id, $snippet->network ); + if ( is_string( $result ) ) { + wp_send_json_error( + array( + 'type' => 'action_error', + 'message' => $result, + ) + ); + } + } else { + deactivate_snippet( $snippet->id, $snippet->network ); + } + } + + wp_send_json_success(); + } +} diff --git a/src/php/admin-menus/class-manage-menu.php b/src/php/admin-menus/class-manage-menu.php index 4ac2b438..17dd318d 100644 --- a/src/php/admin-menus/class-manage-menu.php +++ b/src/php/admin-menus/class-manage-menu.php @@ -14,18 +14,14 @@ class Manage_Menu extends Admin_Menu { /** - * Instance of the list table class. - * - * @var List_Table + * Handle for JavaScript asset file. */ - public List_Table $list_table; + const JS_HANDLE = 'code-snippets-manage-menu'; /** - * Instance of the cloud list table class for search results. - * - * @var Cloud_Search_List_Table + * Handle for CSS asset file. */ - public Cloud_Search_List_Table $cloud_search_list_table; + const CSS_HANDLE = 'code-snippets-manage'; /** * Class constructor @@ -167,19 +163,13 @@ public function register_compact_menu() { } /** - * Executed when the admin page is loaded + * Executed when the admin page is loaded. */ public function load() { parent::load(); - $contextual_help = new Contextual_Help( 'manage' ); + $contextual_help = new Contextual_Help( 'edit' ); $contextual_help->load(); - - $this->cloud_search_list_table = new Cloud_Search_List_Table(); - $this->cloud_search_list_table->prepare_items(); - - $this->list_table = new List_Table(); - $this->list_table->prepare_items(); } /** @@ -190,45 +180,52 @@ public function enqueue_assets() { $rtl = is_rtl() ? '-rtl' : ''; wp_enqueue_style( - 'code-snippets-manage', + self::CSS_HANDLE, plugins_url( "dist/manage$rtl.css", $plugin->file ), - [], + [ + 'wp-components', + ], $plugin->version ); wp_enqueue_script( - 'code-snippets-manage-js', + self::JS_HANDLE, plugins_url( 'dist/manage.js', $plugin->file ), - [ 'wp-i18n' ], + [ + 'react', + 'react-dom', + 'wp-url', + 'wp-i18n', + 'wp-components', + ], $plugin->version, true ); - wp_set_script_translations( 'code-snippets-manage-js', 'code-snippets' ); + Front_End::enqueue_all_prism_themes(); - if ( 'cloud' === $this->get_current_type() || 'cloud_search' === $this->get_current_type() ) { - Front_End::enqueue_all_prism_themes(); - } - } + wp_set_script_translations( self::JS_HANDLE, 'code-snippets' ); + $plugin->localize_script( self::JS_HANDLE ); - /** - * Get the currently displayed snippet type. - * - * @return string - */ - protected function get_current_type(): string { - $types = Plugin::get_types(); - $current_type = isset( $_GET['type'] ) ? sanitize_key( wp_unslash( $_GET['type'] ) ) : 'all'; - return isset( $types[ $current_type ] ) ? $current_type : 'all'; + wp_localize_script( + self::JS_HANDLE, + 'CODE_SNIPPETS_MANAGE', + [ + 'pageTitleActions' => $plugin->is_compact_menu() ? $this->page_title_action_links( [ 'add', 'import', 'settings' ] ) : [], + ] + ); } /** - * Print the status and error messages + * Render the snippets table interface. * * @return void */ - protected function print_messages() { - $this->render_view( 'partials/list-table-notices' ); + public function render() { + printf( + '
%s
', + esc_html__( 'Loading snippets table…', 'code-snippets' ) + ); } /** @@ -243,101 +240,4 @@ protected function print_messages() { public function save_screen_option( $status, string $option, $value ) { return 'snippets_per_page' === $option ? $value : $status; } - - /** - * Update the priority value for a snippet. - * - * @param Snippet $snippet Snippet to update. - * - * @return void - */ - private function update_snippet_priority( Snippet $snippet ) { - global $wpdb; - $table = code_snippets()->db->get_table_name( $snippet->network ); - - $wpdb->update( - $table, - array( 'priority' => $snippet->priority ), - array( 'id' => $snippet->id ), - array( '%d' ), - array( '%d' ) - ); - - clean_snippets_cache( $table ); - } - - /** - * Handle AJAX requests - */ - public function ajax_callback() { - check_ajax_referer( 'code_snippets_manage_ajax' ); - - if ( ! isset( $_POST['field'], $_POST['snippet'] ) ) { - wp_send_json_error( - array( - 'type' => 'param_error', - 'message' => 'incomplete request', - ) - ); - } - - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized - $snippet_data = array_map( 'sanitize_text_field', json_decode( wp_unslash( $_POST['snippet'] ), true ) ); - - $snippet = new Snippet( $snippet_data ); - $field = sanitize_key( $_POST['field'] ); - - if ( 'priority' === $field ) { - - if ( ! isset( $snippet_data['priority'] ) || ! is_numeric( $snippet_data['priority'] ) ) { - wp_send_json_error( - array( - 'type' => 'param_error', - 'message' => 'missing snippet priority data', - ) - ); - } - - $this->update_snippet_priority( $snippet ); - - } elseif ( 'active' === $field ) { - - if ( ! isset( $snippet_data['active'] ) ) { - wp_send_json_error( - array( - 'type' => 'param_error', - 'message' => 'missing snippet active data', - ) - ); - } - - if ( $snippet->shared_network ) { - $active_shared_snippets = get_option( 'active_shared_network_snippets', array() ); - - if ( in_array( $snippet->id, $active_shared_snippets, true ) !== $snippet->active ) { - - $active_shared_snippets = $snippet->active ? - array_merge( $active_shared_snippets, array( $snippet->id ) ) : - array_diff( $active_shared_snippets, array( $snippet->id ) ); - - update_option( 'active_shared_network_snippets', $active_shared_snippets ); - clean_active_snippets_cache( code_snippets()->db->ms_table ); - } - } elseif ( $snippet->active ) { - $result = activate_snippet( $snippet->id, $snippet->network ); - if ( is_string( $result ) ) { - wp_send_json_error( - array( - 'type' => 'action_error', - 'message' => $result, - ) - ); - } - } else { - deactivate_snippet( $snippet->id, $snippet->network ); - } - } - - wp_send_json_success(); - } } diff --git a/src/php/class-admin.php b/src/php/class-admin.php index b6ffa63e..58392726 100644 --- a/src/php/class-admin.php +++ b/src/php/class-admin.php @@ -41,7 +41,8 @@ public function __construct() { * Initialise classes */ public function load_classes() { - $this->menus['manage'] = new Manage_Menu(); + $this->menus['manage_new'] = new Manage_Menu(); + $this->menus['manage'] = new Manage_Menu_Legacy(); $this->menus['edit'] = new Edit_Menu(); $this->menus['import'] = new Import_Menu(); diff --git a/src/php/class-plugin.php b/src/php/class-plugin.php index a89103ba..efba5854 100644 --- a/src/php/class-plugin.php +++ b/src/php/class-plugin.php @@ -175,6 +175,10 @@ public function get_menu_slug( string $menu = '' ): string { $cloud = array( 'cloud', 'cloud-snippets' ); $welcome = array( 'welcome', 'getting-started', 'code-snippets' ); + if ( 'manage-legacy' === $menu ) { + return 'snippets-legacy'; + } + if ( in_array( $menu, $edit, true ) ) { return 'edit-snippet'; } elseif ( in_array( $menu, $add, true ) ) { @@ -359,10 +363,10 @@ public function localize_script( string $handle ) { 'localToken' => $this->cloud_api->get_local_token(), ], 'urls' => [ - 'plugin' => esc_url_raw( plugins_url( '', PLUGIN_FILE ) ), - 'manage' => esc_url_raw( $this->get_menu_url() ), - 'edit' => esc_url_raw( $this->get_menu_url( 'edit' ) ), - 'addNew' => esc_url_raw( $this->get_menu_url( 'add' ) ), + 'plugin' => esc_url_raw( plugins_url( '', PLUGIN_FILE ) ), + 'manage' => esc_url_raw( $this->get_menu_url() ), + 'edit' => esc_url_raw( $this->get_menu_url( 'edit' ) ), + 'addNew' => esc_url_raw( $this->get_menu_url( 'add' ) ), ], ] ); diff --git a/src/php/views/manage.php b/src/php/views/manage.php index 4533bfdd..d5298ceb 100644 --- a/src/php/views/manage.php +++ b/src/php/views/manage.php @@ -13,7 +13,7 @@ /** * Loaded from the manage menu class. * - * @var Manage_Menu $this + * @var Manage_Menu_Legacy $this */ if ( ! defined( 'ABSPATH' ) ) { diff --git a/src/php/views/partials/cloud-search.php b/src/php/views/partials/cloud-search.php index 078399ed..3be462b9 100644 --- a/src/php/views/partials/cloud-search.php +++ b/src/php/views/partials/cloud-search.php @@ -11,7 +11,7 @@ /** * Loaded from manage menu. * - * @var Manage_Menu $this + * @var Manage_Menu_Legacy $this */ $search_query = isset( $_REQUEST['cloud_search'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['cloud_search'] ) ) : ''; diff --git a/src/php/views/partials/list-table-notices.php b/src/php/views/partials/list-table-notices.php index e8dbc99d..8e3de635 100644 --- a/src/php/views/partials/list-table-notices.php +++ b/src/php/views/partials/list-table-notices.php @@ -11,7 +11,7 @@ /** * Loaded from the manage menu. * - * @var Manage_Menu $this + * @var Manage_Menu_Legacy $this */ if ( defined( 'CODE_SNIPPETS_SAFE_MODE' ) && CODE_SNIPPETS_SAFE_MODE ) { diff --git a/src/php/views/partials/list-table.php b/src/php/views/partials/list-table.php index a9569386..cf02265b 100644 --- a/src/php/views/partials/list-table.php +++ b/src/php/views/partials/list-table.php @@ -11,7 +11,7 @@ /** * Loaded from the manage menu. * - * @var Manage_Menu $this + * @var Manage_Menu_Legacy $this */ ?> diff --git a/src/readme.txt b/src/readme.txt index 6eedcee3..79d53be3 100644 --- a/src/readme.txt +++ b/src/readme.txt @@ -4,7 +4,7 @@ Donate link: https://codesnippets.pro Tags: code, snippets, multisite, php, css License: GPL-2.0-or-later License URI: license.txt -Stable tag: 3.7.0-beta.1 +Stable tag: 3.8.0-dev.1 Tested up to: 6.7.2 An easy, clean and simple way to enhance your site with code snippets. From 846bb819cfa1359bb13ecfd6a873dba13bdf894a Mon Sep 17 00:00:00 2001 From: Shea Bunge Date: Thu, 24 Jul 2025 18:40:32 +1000 Subject: [PATCH 042/318] Fold shortcodes utils into component file. --- .../EditorSidebar/actions/ShortcodeInfo.tsx | 13 +++++++++++-- src/js/utils/shortcodes.ts | 10 ---------- 2 files changed, 11 insertions(+), 12 deletions(-) delete mode 100644 src/js/utils/shortcodes.ts diff --git a/src/js/components/EditorSidebar/actions/ShortcodeInfo.tsx b/src/js/components/EditorSidebar/actions/ShortcodeInfo.tsx index 1e0aa0cc..2d1ad820 100644 --- a/src/js/components/EditorSidebar/actions/ShortcodeInfo.tsx +++ b/src/js/components/EditorSidebar/actions/ShortcodeInfo.tsx @@ -2,11 +2,20 @@ import React, { useState } from 'react' import { ExternalLink } from '@wordpress/components' import { __ } from '@wordpress/i18n' import { useSnippetForm } from '../../../hooks/useSnippetForm' -import { buildShortcodeTag } from '../../../utils/shortcodes' import { CopyToClipboardButton } from '../../common/CopyToClipboardButton' -import type { ShortcodeAtts } from '../../../utils/shortcodes' import type { Dispatch, SetStateAction} from 'react' +type ShortcodeAtts = Record + +const buildShortcodeTag = (tag: string, atts: ShortcodeAtts): string => + `[${[ + tag, + ...Object.entries(atts) + .filter(([, value]) => Boolean(value)) + .map(([att, value]) => + 'boolean' === typeof value ? att : `${att}=${JSON.stringify(value)}`) + ].filter(Boolean).join(' ')}]` + const SHORTCODE_TAG = 'code_snippet' interface ShortcodeOptions { diff --git a/src/js/utils/shortcodes.ts b/src/js/utils/shortcodes.ts deleted file mode 100644 index 56542c89..00000000 --- a/src/js/utils/shortcodes.ts +++ /dev/null @@ -1,10 +0,0 @@ -export type ShortcodeAtts = Record - -export const buildShortcodeTag = (tag: string, atts: ShortcodeAtts): string => - `[${[ - tag, - ...Object.entries(atts) - .filter(([, value]) => Boolean(value)) - .map(([att, value]) => - 'boolean' === typeof value ? att : `${att}=${JSON.stringify(value)}`) - ].filter(Boolean).join(' ')}]` From 46ec4a75473933c29692b334af230a1cefd6048f Mon Sep 17 00:00:00 2001 From: Shea Bunge Date: Thu, 24 Jul 2025 18:41:01 +1000 Subject: [PATCH 043/318] Consoldate legacy manage styles into a single file. --- src/css/manage-legacy.scss | 342 +++++++++++++++++++++++++++++- src/css/manage-legacy/_cloud.scss | 340 ----------------------------- 2 files changed, 341 insertions(+), 341 deletions(-) delete mode 100644 src/css/manage-legacy/_cloud.scss diff --git a/src/css/manage-legacy.scss b/src/css/manage-legacy.scss index d7a9972c..11055c8b 100644 --- a/src/css/manage-legacy.scss +++ b/src/css/manage-legacy.scss @@ -8,7 +8,6 @@ @use 'common/badges'; @use 'common/switch'; @use 'common/select'; -@use 'manage-legacy/cloud'; .column-name, .column-type { @@ -211,3 +210,344 @@ td.column-description { } } } + +/** Cloud */ + + +.cloud-legend-tooltip { + h3 { + font-size: 16px; + color: #fff; + text-align: center; + } + + td { + vertical-align: top; + } +} + +.cloud-search-info { + text-align: justify; + + small { + color: #646970; + float: inline-end; + } +} + +.thickbox-code-viewer { + min-block-size: 250px; + background-color: hsl(0deg 0% 96.5%); + padding: 20px; + border-radius: 10px; +} + +#snippet-code-thickbox { + display: block; + inline-size: 100%; +} + +.no-results { + font-size: 15px; +} + +.dashicons.cloud-synced { + color: theme.$cloud; +} + +.dashicons.cloud-downloaded { + color: #e91e63; +} + +.dashicons.cloud-not-downloaded { + color: theme.$outline; +} + +.dashicons.cloud-update { + color: theme.$cloud-update; +} + +.cloud_update a { + color: theme.$cloud-update !important; + text-decoration: underline; +} + +.updated.column-updated span { + text-decoration: dotted underline; +} + +td.column-name { + .cloud-icon { + margin-inline-end: 3px; + } +} + +td.column-download { + display: flex; + gap: 0.5em; + flex-flow: column; + text-align: center; +} + +.cloud-snippet-download { + color: theme.$accent !important; +} + +.cloud-snippet-downloaded, .cloud-snippet-preview-style { + color: #616161 !important; +} + +.cloud-snippet-update { + color: theme.$cloud-update !important; +} + +#cloud-search-form { + margin-block: 30px; + text-align: center; +} + +.input-group { + position: relative; + display: flex; + flex-wrap: wrap; + align-items: stretch; + max-inline-size: 900px; + margin: 0 auto; +} + +#cloud_search { + display: block; + padding: 0.375rem 0.75rem; + font-size: 1rem; + color: #495057; + background-clip: padding-box; + border-radius: 0; + transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out; + position: relative; + flex: 1 1 auto; + inline-size: 1%; + margin-block-end: 0; + + &:focus { + outline: 0; + border: 1px solid #8c8f94; + box-shadow: none; + } +} + +#cloud-select-prepend { + margin-inline-end: -3px; + border-start-end-radius: 0; + border-end-end-radius: 0; + position: relative; + z-index: 2; + color: theme.$accent; + border-color: theme.$accent; + background-color: #f6f7f7; + + &:hover { + background-color: #f0f0f1; + border-color: #0a4b78; + color: #0a4b78; + } +} + +#cloud-search-submit { + padding: 0 15px; + margin-inline-start: -3px; + display: flex; + justify-content: center; + align-items: center; +} + +.cloud-search { + margin-inline-start: 5px; +} + +.bundle-group { + margin-block-start: 10px; + justify-content: space-between; + display: flex; + gap: 5px; + flex-wrap: nowrap; +} + +#cloud-bundles { + color: #495057; + display: flex; + flex: 1 1 auto; + font-size: 1rem; + padding: 0.375rem 0.75rem; + position: relative; + inline-size: 50%; +} + +#cloud-bundle-show { + inline-size: 10%; +} + +#cloud-bundle-run { + inline-size: 15%; +} + +#bundle_share_name { + color: #495057; + font-size: 1rem; + inline-size: 25%; +} + +.heading-box { + max-inline-size: 900px; + margin: auto; + padding-block-end: 1rem; +} + +.cloud-search-heading { + font-size: 23px; + font-weight: 400; + padding: 9px 0 4px; + line-height: 1.3; + text-align: center; + margin-block-end: 0; +} + +.cloud-search-card .badge { + block-size: 34px; +} + +.cloud-badge.ai-icon { + font-size: 12px; + padding: 3px; + margin-inline-start: 5px; + color: #b22222; +} + +.cloud-search-card-bottom { + min-block-size: 40px; +} + +#cloud-search-results .cloud-snippets #the-list { + display: flex; + flex-wrap: wrap; + justify-content: center; +} + +.cloud-snippets .plugin-card { + display: flex; + flex-direction: column; + justify-content: space-between; +} + +.cloud-connect-wrap { + display: flex; + justify-content: space-between; + align-items: center; + max-block-size: 35px; + margin: 0 3px; + float: inline-end; + gap: 5px; +} + + +.cloud-table > tbody > tr { + block-size: 80px; + box-shadow: inset 0 -1px 0 rgb(0 0 0 / 10%); +} + +.cloud-table > tbody > tr > td { + max-inline-size: 250px; +} + +.cloud-table tbody .active-snippet .column-name { + font-weight: 400; + max-inline-size: 400px; + white-space: normal !important; +} + +.cloud-table td .no-results { + margin-block-start: 15px; + color: #e32121; + text-align: center; +} + +.cloud-status-dot { + block-size: 10px; + inline-size: 10px; + background-color: #ce0000; + border-radius: 50%; + + + .cloud-connect-active & { + background-color: #25a349; + } +} + +.cloud-connect-text { + color: #ce0000; + + .cloud-connect-active & { + color: #2e7d32; + } +} + +.thumbs-up { + inline-size: 1.25rem; /* 20px */ + block-size: 1.25rem; /* 20px */ + transform-origin: bottom left; + + &:hover { + stroke: #059669; + fill: #6ee7b7; + } +} + +.voted-info { + display: inline-flex; + gap: 3px; + align-items: center; + margin-block-end: 6px !important; + + &:hover { + .thumbs-up { + stroke: #059669; + fill: #6ee7b7; + animation: thumb 1s ease-in-out infinite; + } + } +} + +.plugin-card-bottom { + overflow: visible !important; +} + +.beta-test-notice { + margin-block-start: 20px; +} + +.highlight-yellow { + background: #ffee58; + padding: 3px; + border-radius: 3px; +} + +@keyframes thumb { + 0% { + transform: rotate(0) + } + + 33% { + transform: rotate(7deg) + } + + 66% { + transform: rotate(-15deg) + } + + 90% { + transform: rotate(5deg) + } + + 100% { + transform: rotate(0) + } +} diff --git a/src/css/manage-legacy/_cloud.scss b/src/css/manage-legacy/_cloud.scss deleted file mode 100644 index e72fab37..00000000 --- a/src/css/manage-legacy/_cloud.scss +++ /dev/null @@ -1,340 +0,0 @@ -@use '../common/theme'; -@use '../common/tooltips'; - -.cloud-legend-tooltip { - h3 { - font-size: 16px; - color: #fff; - text-align: center; - } - - td { - vertical-align: top; - } -} - -.cloud-search-info { - text-align: justify; - - small { - color: #646970; - float: inline-end; - } -} - -.thickbox-code-viewer { - min-block-size: 250px; - background-color: hsl(0deg 0% 96.5%); - padding: 20px; - border-radius: 10px; -} - -#snippet-code-thickbox { - display: block; - inline-size: 100%; -} - -.no-results { - font-size: 15px; -} - -.dashicons.cloud-synced { - color: theme.$cloud; -} - -.dashicons.cloud-downloaded { - color: #e91e63; -} - -.dashicons.cloud-not-downloaded { - color: theme.$outline; -} - -.dashicons.cloud-update { - color: theme.$cloud-update; -} - -.cloud_update a { - color: theme.$cloud-update !important; - text-decoration: underline; -} - -.updated.column-updated span { - text-decoration: dotted underline; -} - -td.column-name { - .cloud-icon { - margin-inline-end: 3px; - } -} - -td.column-download { - display: flex; - gap: 0.5em; - flex-flow: column; - text-align: center; -} - -.cloud-snippet-download { - color: theme.$accent !important; -} - -.cloud-snippet-downloaded, .cloud-snippet-preview-style { - color: #616161 !important; -} - -.cloud-snippet-update { - color: theme.$cloud-update !important; -} - -#cloud-search-form { - margin-block: 30px; - text-align: center; -} - -.input-group { - position: relative; - display: flex; - flex-wrap: wrap; - align-items: stretch; - max-inline-size: 900px; - margin: 0 auto; -} - -#cloud_search { - display: block; - padding: 0.375rem 0.75rem; - font-size: 1rem; - color: #495057; - background-clip: padding-box; - border-radius: 0; - transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out; - position: relative; - flex: 1 1 auto; - inline-size: 1%; - margin-block-end: 0; - - &:focus { - outline: 0; - border: 1px solid #8c8f94; - box-shadow: none; - } -} - -#cloud-select-prepend { - margin-inline-end: -3px; - border-start-end-radius: 0; - border-end-end-radius: 0; - position: relative; - z-index: 2; - color: theme.$accent; - border-color: theme.$accent; - background-color: #f6f7f7; - - &:hover { - background-color: #f0f0f1; - border-color: #0a4b78; - color: #0a4b78; - } -} - -#cloud-search-submit { - padding: 0 15px; - margin-inline-start: -3px; - display: flex; - justify-content: center; - align-items: center; -} - -.cloud-search { - margin-inline-start: 5px; -} - -.bundle-group { - margin-block-start: 10px; - justify-content: space-between; - display: flex; - gap: 5px; - flex-wrap: nowrap; -} - -#cloud-bundles { - color: #495057; - display: flex; - flex: 1 1 auto; - font-size: 1rem; - padding: 0.375rem 0.75rem; - position: relative; - inline-size: 50%; -} - -#cloud-bundle-show { - inline-size: 10%; -} - -#cloud-bundle-run { - inline-size: 15%; -} - -#bundle_share_name { - color: #495057; - font-size: 1rem; - inline-size: 25%; -} - -.heading-box { - max-inline-size: 900px; - margin: auto; - padding-block-end: 1rem; -} - -.cloud-search-heading { - font-size: 23px; - font-weight: 400; - padding: 9px 0 4px; - line-height: 1.3; - text-align: center; - margin-block-end: 0; -} - -.cloud-search-card .badge { - block-size: 34px; -} - -.cloud-badge.ai-icon { - font-size: 12px; - padding: 3px; - margin-inline-start: 5px; - color: #b22222; -} - -.cloud-search-card-bottom { - min-block-size: 40px; -} - -#cloud-search-results .cloud-snippets #the-list { - display: flex; - flex-wrap: wrap; - justify-content: center; -} - -.cloud-snippets .plugin-card { - display: flex; - flex-direction: column; - justify-content: space-between; -} - -.cloud-connect-wrap { - display: flex; - justify-content: space-between; - align-items: center; - max-block-size: 35px; - margin: 0 3px; - float: inline-end; - gap: 5px; -} - - -.cloud-table > tbody > tr { - block-size: 80px; - box-shadow: inset 0 -1px 0 rgb(0 0 0 / 10%); -} - -.cloud-table > tbody > tr > td { - max-inline-size: 250px; -} - -.cloud-table tbody .active-snippet .column-name { - font-weight: 400; - max-inline-size: 400px; - white-space: normal !important; -} - -.cloud-table td .no-results { - margin-block-start: 15px; - color: #e32121; - text-align: center; -} - -.cloud-status-dot { - block-size: 10px; - inline-size: 10px; - background-color: #ce0000; - border-radius: 50%; - - - .cloud-connect-active & { - background-color: #25a349; - } -} - -.cloud-connect-text { - color: #ce0000; - - .cloud-connect-active & { - color: #2e7d32; - } -} - -.thumbs-up { - inline-size: 1.25rem; /* 20px */ - block-size: 1.25rem; /* 20px */ - transform-origin: bottom left; - - &:hover { - stroke: #059669; - fill: #6ee7b7; - } -} - -.voted-info { - display: inline-flex; - gap: 3px; - align-items: center; - margin-block-end: 6px !important; - - &:hover { - .thumbs-up { - stroke: #059669; - fill: #6ee7b7; - animation: thumb 1s ease-in-out infinite; - } - } -} - -.plugin-card-bottom { - overflow: visible !important; -} - -.beta-test-notice { - margin-block-start: 20px; -} - -.highlight-yellow { - background: #ffee58; - padding: 3px; - border-radius: 3px; -} - -@keyframes thumb { - 0% { - transform: rotate(0) - } - - 33% { - transform: rotate(7deg) - } - - 66% { - transform: rotate(-15deg) - } - - 90% { - transform: rotate(5deg) - } - - 100% { - transform: rotate(0) - } -} From a69aa7390de703603a7ec58c3d1af1cc706f308f Mon Sep 17 00:00:00 2001 From: Shea Bunge Date: Thu, 24 Jul 2025 18:41:56 +1000 Subject: [PATCH 044/318] Only output request debug messages when WP_DEBUG is enabled. --- src/js/hooks/useRestAPI.tsx | 12 ++++++++---- src/js/types/Window.ts | 1 + src/php/class-plugin.php | 1 + 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/js/hooks/useRestAPI.tsx b/src/js/hooks/useRestAPI.tsx index 4e945095..ece0ef5f 100644 --- a/src/js/hooks/useRestAPI.tsx +++ b/src/js/hooks/useRestAPI.tsx @@ -26,10 +26,14 @@ const debugRequest = async ( doRequest: Promise>, data?: D ): Promise => { - console.debug(`${method} ${url}`, ...data ? [data] : []) - const response = await doRequest - console.debug('Response', response) - return response.data + if (window.CODE_SNIPPETS?.debug) { + console.debug(`${method} ${url}`, ...data ? [data] : []) + const response = await doRequest + console.debug('Response', response) + return response.data + } else { + return (await doRequest).data + } } const buildRestAPI = (axiosInstance: AxiosInstance): RestAPI => ({ diff --git a/src/js/types/Window.ts b/src/js/types/Window.ts index f32f2a18..027f0505 100644 --- a/src/js/types/Window.ts +++ b/src/js/types/Window.ts @@ -18,6 +18,7 @@ declare global { readonly code_snippets_editor_settings: EditorOption[] CODE_SNIPPETS_PRISM?: typeof Prism readonly CODE_SNIPPETS?: { + debug: boolean isLicensed: boolean restAPI: { base: string diff --git a/src/php/class-plugin.php b/src/php/class-plugin.php index efba5854..973281d0 100644 --- a/src/php/class-plugin.php +++ b/src/php/class-plugin.php @@ -354,6 +354,7 @@ public function localize_script( string $handle ) { $handle, 'CODE_SNIPPETS', [ + 'debug' => defined( 'WP_DEBUG' ) && WP_DEBUG, 'isLicensed' => $this->licensing->is_licensed(), 'isCloudConnected' => Cloud_API::is_cloud_connection_available(), 'restAPI' => [ From 9f236c82404e48502c1131a13eb35e5bb2196916 Mon Sep 17 00:00:00 2001 From: Shea Bunge Date: Thu, 24 Jul 2025 18:43:14 +1000 Subject: [PATCH 045/318] Clean up leftover conditions code. --- src/js/components/common/SubmitButton.tsx | 3 + src/js/utils/snippets.ts | 80 ----------------------- src/js/utils/snippets/api.ts | 14 ++-- src/js/utils/snippets/objects.ts | 79 ++++++---------------- src/js/utils/snippets/snippets.ts | 7 +- src/php/snippet-ops.php | 8 --- 6 files changed, 28 insertions(+), 163 deletions(-) delete mode 100644 src/js/utils/snippets.ts diff --git a/src/js/components/common/SubmitButton.tsx b/src/js/components/common/SubmitButton.tsx index 5c6bfeac..86ee371b 100644 --- a/src/js/components/common/SubmitButton.tsx +++ b/src/js/components/common/SubmitButton.tsx @@ -7,6 +7,7 @@ export interface SubmitButtonProps extends Omit = ({ text, name = 'submit', primary, + secondary, small, large, wrap, @@ -34,6 +36,7 @@ export const SubmitButton: React.FC = ({ 'button', { 'button-primary': primary, + 'button-secondary': secondary, 'button-small': small, 'button-large': large }, diff --git a/src/js/utils/snippets.ts b/src/js/utils/snippets.ts deleted file mode 100644 index 63c167f3..00000000 --- a/src/js/utils/snippets.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { SNIPPET_TYPE_SCOPES } from '../types/Snippet' -import { isNetworkAdmin } from './screen' -import type { Snippet, SnippetScope, SnippetType } from '../types/Snippet' - -const PRO_TYPES: SnippetType[] = ['css', 'js'] - -const defaults: Omit = { - id: 0, - name: '', - code: '', - desc: '', - scope: 'global', - modified: '', - active: false, - network: isNetworkAdmin(), - shared_network: null, - priority: 10, - conditionId: 0 -} - -const isAbsInt = (value: unknown): value is number => - 'number' === typeof value && 0 < value - -const isBooleanOrUndefined = (value: unknown): value is boolean | undefined => - 'boolean' === typeof value || value === undefined - -const parseStringArray = (value: unknown): string[] | undefined => - Array.isArray(value) ? value.filter(entry => 'string' === typeof entry) : undefined - -export const isValidScope = (scope: unknown): scope is SnippetScope => - 'string' === typeof scope && Object.values(SNIPPET_TYPE_SCOPES).some(typeScopes => - typeScopes.some(typeScope => typeScope === scope)) - -export const createSnippetObject = (fields: unknown = undefined): Snippet => { - if ('object' !== typeof fields || null === fields) { - return { ...defaults, tags: [] } - } - - return { - id: 'id' in fields && isAbsInt(fields.id) ? fields.id : defaults.id, - name: 'name' in fields && 'string' === typeof fields.name ? fields.name : defaults.name, - desc: 'desc' in fields && 'string' === typeof fields.desc ? fields.desc : defaults.desc, - code: 'code' in fields && 'string' === typeof fields.code ? fields.code : defaults.code, - tags: 'tags' in fields ? parseStringArray(fields.tags) ?? [] : [], - scope: 'scope' in fields && isValidScope(fields.scope) ? fields.scope : defaults.scope, - modified: 'modified' in fields && 'string' === typeof fields.modified ? fields.modified : defaults.modified, - active: 'active' in fields && 'boolean' === typeof fields.active ? fields.active : defaults.active, - network: 'network' in fields && 'boolean' === typeof fields.network ? fields.network : defaults.network, - shared_network: 'shared_network' in fields && isBooleanOrUndefined(fields.shared_network) - ? fields.shared_network : defaults.shared_network, - priority: 'priority' in fields && 'number' === typeof fields.priority ? fields.priority : defaults.priority, - conditionId: 'conditionId' in fields && isAbsInt(fields.conditionId) ? fields.conditionId - : 'condition_id' in fields && isAbsInt(fields.condition_id) ? fields.condition_id - : defaults.conditionId - } -} - -export const getSnippetType = (snippetOrScope: Snippet | SnippetScope): SnippetType => { - const scope = 'string' === typeof snippetOrScope ? snippetOrScope : snippetOrScope.scope - - switch (true) { - case scope.endsWith('-css'): - return 'css' - - case scope.endsWith('-js'): - return 'js' - - case scope.endsWith('content'): - return 'html' - - default: - return 'php' - } -} - -export const isProSnippet = (snippet: Snippet | SnippetScope): boolean => - PRO_TYPES.includes(getSnippetType(snippet)) - -export const isProType = (type: SnippetType): boolean => - PRO_TYPES.includes(type) diff --git a/src/js/utils/snippets/api.ts b/src/js/utils/snippets/api.ts index 635c3240..5094d8f0 100644 --- a/src/js/utils/snippets/api.ts +++ b/src/js/utils/snippets/api.ts @@ -1,6 +1,6 @@ import { addQueryArgs } from '@wordpress/url' import { REST_SNIPPETS_BASE } from '../restAPI' -import { createSnippetObject, isCondition } from './snippets' +import { createSnippetObject } from './snippets' import type { RestAPI } from '../../hooks/useRestAPI' import type { SnippetSchema, WritableSnippetSchema } from '../../types/schema/SnippetSchema' import type { Snippet } from '../../types/Snippet' @@ -10,7 +10,7 @@ export interface SnippetsAPI { fetchAll: (network?: boolean | null) => Promise fetch: (snippetId: number, network?: boolean | null) => Promise create: (snippet: Snippet) => Promise - update: (snippet: Snippet) => Promise + update: (snippet: Pick & Partial) => Promise delete: (snippet: Pick) => Promise activate: (snippet: Pick) => Promise deactivate: (snippet: Pick) => Promise @@ -36,16 +36,10 @@ const mapToSchema = ({ active, network, conditionId, - conditions -}: Snippet): WritableSnippetSchema => ({ +}: Partial): WritableSnippetSchema => ({ name, desc, - code: isCondition({ scope }) - ? JSON.stringify( - Object.values(conditions) - .map(group => group && Object.values(group)) - .filter(group => Array.isArray(group) && group.length > 0)) - : code, + code, tags, scope, priority, diff --git a/src/js/utils/snippets/objects.ts b/src/js/utils/snippets/objects.ts index 37e0e50a..09c9c272 100644 --- a/src/js/utils/snippets/objects.ts +++ b/src/js/utils/snippets/objects.ts @@ -1,6 +1,5 @@ -import { SNIPPET_TYPE_SCOPES } from '../../types/Snippet' import type { Snippet, SnippetScope } from '../../types/Snippet' -import type { ConditionGroups } from '../../types/ConditionGroups' +import { SNIPPET_TYPE_SCOPES } from '../../types/Snippet' const isAbsInt = (value: unknown): value is number => 'number' === typeof value && 0 < value @@ -12,62 +11,22 @@ export const isValidScope = (scope: unknown): scope is SnippetScope => 'string' === typeof scope && Object.values(SNIPPET_TYPE_SCOPES).some(typeScopes => typeScopes.some(typeScope => typeScope === scope)) -const isValidCondition = (condition: unknown): condition is ConditionGroups => - 'object' === typeof condition && null !== condition && Object.values(condition) - .every((group: unknown) => 'object' === typeof group && null !== group && - Object.values(group).every((rule: unknown) => 'object' === typeof rule && null !== rule)) - -const generateObjectKeys = (items: Record, transformItem?: (item: T) => T): Record => - Object.fromEntries( - Object.values(items).map((item, index) => - [`_${index}`, transformItem ? transformItem(item) : item] - )) - -const parseConditionGroups = (condition: ConditionGroups) => - generateObjectKeys(condition, group => group && generateObjectKeys(group)) - -const parseConditions = (parsed: Snippet, fields: object): Partial => { - if ('conditions' in fields && isValidCondition(fields.conditions)) { - return { conditions: parseConditionGroups(fields.conditions) } - } - - if ('condition' === parsed.scope && '' !== parsed.code.trim()) { - try { - const parsedRules: unknown = JSON.parse(parsed.code) - - if (isValidCondition(parsedRules)) { - return { conditions: parseConditionGroups(parsedRules), code: '' } - } - } catch (error) { - console.error('Failed to parse condition rules JSON.', parsed.code, error) +export const parseSnippetObject = (fields: unknown, defaults: Snippet): Snippet => + 'object' !== typeof fields || null === fields + ? defaults + : { + id: 'id' in fields && isAbsInt(fields.id) ? fields.id : defaults.id, + name: 'name' in fields && 'string' === typeof fields.name ? fields.name : defaults.name, + desc: 'desc' in fields && 'string' === typeof fields.desc ? fields.desc : defaults.desc, + code: 'code' in fields && 'string' === typeof fields.code ? fields.code : defaults.code, + tags: 'tags' in fields ? parseStringArray(fields.tags) ?? defaults.tags : defaults.tags, + scope: 'scope' in fields && isValidScope(fields.scope) ? fields.scope : defaults.scope, + modified: 'modified' in fields && 'string' === typeof fields.modified ? fields.modified : defaults.modified, + active: 'active' in fields && 'boolean' === typeof fields.active ? fields.active : defaults.active, + network: 'network' in fields && 'boolean' === typeof fields.network ? fields.network : defaults.network, + shared_network: 'shared_network' in fields && 'boolean' === typeof fields.shared_network && fields.shared_network || + defaults.shared_network, + priority: 'priority' in fields && 'number' === typeof fields.priority ? fields.priority : defaults.priority, + conditionId: 'condition_id' in fields && isAbsInt(fields.condition_id) ? fields.condition_id + : 'conditionId' in fields && isAbsInt(fields.conditionId) ? fields.conditionId : defaults.conditionId } - } - - return {} -} - -export const parseSnippetObject = (fields: unknown, defaults: Snippet): Snippet => { - if ('object' !== typeof fields || null === fields) { - return defaults - } - - const parsed: Snippet = { - id: 'id' in fields && isAbsInt(fields.id) ? fields.id : defaults.id, - name: 'name' in fields && 'string' === typeof fields.name ? fields.name : defaults.name, - desc: 'desc' in fields && 'string' === typeof fields.desc ? fields.desc : defaults.desc, - code: 'code' in fields && 'string' === typeof fields.code ? fields.code : defaults.code, - tags: 'tags' in fields ? parseStringArray(fields.tags) ?? defaults.tags : defaults.tags, - scope: 'scope' in fields && isValidScope(fields.scope) ? fields.scope : defaults.scope, - modified: 'modified' in fields && 'string' === typeof fields.modified ? fields.modified : defaults.modified, - active: 'active' in fields && 'boolean' === typeof fields.active ? fields.active : defaults.active, - network: 'network' in fields && 'boolean' === typeof fields.network ? fields.network : defaults.network, - shared_network: 'shared_network' in fields && 'boolean' === typeof fields.shared_network && fields.shared_network || - defaults.shared_network, - priority: 'priority' in fields && 'number' === typeof fields.priority ? fields.priority : defaults.priority, - conditionId: 'condition_id' in fields && isAbsInt(fields.condition_id) ? fields.condition_id - : 'conditionId' in fields && isAbsInt(fields.conditionId) ? fields.conditionId : defaults.conditionId, - conditions: defaults.conditions - } - - return { ...parsed, ...parseConditions(parsed, fields) } -} diff --git a/src/js/utils/snippets/snippets.ts b/src/js/utils/snippets/snippets.ts index 72998e07..20a0fd74 100644 --- a/src/js/utils/snippets/snippets.ts +++ b/src/js/utils/snippets/snippets.ts @@ -20,7 +20,7 @@ const defaults: Omit = { } export const createSnippetObject = (fields: unknown = null): Snippet => - parseSnippetObject(fields, { ...defaults, tags: [], conditions: {} }) + parseSnippetObject(fields, { ...defaults, tags: [] }) export const getSnippetType = ({ scope }: Pick): SnippetType => { switch (true) { @@ -43,10 +43,7 @@ export const getSnippetType = ({ scope }: Pick): SnippetType = export const validateSnippet = (snippet: Snippet): undefined | string => { const missingTitle = '' === snippet.name.trim() - - const missingCode = isCondition(snippet) - ? !snippet.conditions - : '' === snippet.code.trim() + const missingCode = '' === snippet.code.trim() switch (true) { case missingCode && missingTitle: diff --git a/src/php/snippet-ops.php b/src/php/snippet-ops.php index ea5bc625..65420aa8 100644 --- a/src/php/snippet-ops.php +++ b/src/php/snippet-ops.php @@ -675,14 +675,6 @@ function execute_active_snippets(): bool { continue; } - if ( $snippet['condition_id'] ) { - $condition_id = intval( $snippet['condition_id'] ); - - if ( isset( $conditions[ $condition_id ] ) && ! $conditions[ $condition_id ] ) { - continue; - } - } - execute_snippet( $code, $snippet_id ); } } From 320ef3d23acc8a6a89c3aa160763661d288ec26e Mon Sep 17 00:00:00 2001 From: Shea Bunge Date: Thu, 24 Jul 2025 18:43:29 +1000 Subject: [PATCH 046/318] Initial implementation of snippets table in React. --- src/css/manage.scss | 81 ++++++++++ .../SnippetForm/fields/SnippetTypeInput.tsx | 14 +- .../SnippetsTable/SnippetTypeTabs.tsx | 65 ++++++++ .../SnippetsTable/SnippetsListTable.tsx | 151 ++++++++++++++++++ .../SnippetsTable/SnippetsTable.tsx | 41 ++++- .../components/SnippetsTable/TableColumns.tsx | 117 ++++++++++++++ .../components/common/ListTable/ListTable.tsx | 119 ++++++++++++++ .../common/ListTable/TableHeadings.tsx | 102 ++++++++++++ .../common/ListTable/TableItems.tsx | 62 +++++++ .../components/common/ListTable/TableNav.tsx | 116 ++++++++++++++ src/js/components/common/ListTable/index.ts | 1 + src/js/types/Snippet.ts | 7 +- src/js/utils/snippets/snippets.ts | 8 + src/php/admin-menus/class-manage-menu.php | 10 ++ 14 files changed, 882 insertions(+), 12 deletions(-) create mode 100644 src/js/components/SnippetsTable/SnippetTypeTabs.tsx create mode 100644 src/js/components/SnippetsTable/SnippetsListTable.tsx create mode 100644 src/js/components/SnippetsTable/TableColumns.tsx create mode 100644 src/js/components/common/ListTable/ListTable.tsx create mode 100644 src/js/components/common/ListTable/TableHeadings.tsx create mode 100644 src/js/components/common/ListTable/TableItems.tsx create mode 100644 src/js/components/common/ListTable/TableNav.tsx create mode 100644 src/js/components/common/ListTable/index.ts diff --git a/src/css/manage.scss b/src/css/manage.scss index e69de29b..836280ce 100644 --- a/src/css/manage.scss +++ b/src/css/manage.scss @@ -0,0 +1,81 @@ +@use 'common/badges'; +@use 'common/switch'; +@use 'common/select'; + +.wrap h2.nav-tab-wrapper { + display: flex; + flex-flow: row wrap-reverse; + gap: 0.5em; +} + +.nav-tab { + display: flex; + flex-flow: row wrap; + align-items: center; + gap: 8px; + margin: 0; + + @media screen and (width <= 1190px) { + .snippet-label { + display: none; + } + } +} + +.nav-tab-wrapper + .subsubsub, p.search-box { + margin: 10px 0 0; +} + +.wrap .subsubsub { + li::after { + content: ' | '; + } + + li:last-child::after { + content: ''; + } +} + +.priority-column input { + appearance: none; + background: none; + border: none; + box-shadow: none; + inline-size: 4em; + color: #666; + text-align: center; + + &:hover, &:focus, &:active { + color: #000; + background-color: rgb(0 0 0 / 10%); + border-radius: 6px; + appearance: unset; + } + + &:disabled { + color: inherit; + } +} + +.wp-list-table { + td.column-id { + text-align: center; + } + + tr { + background: #fff; + } + + ol, ul { + margin: 0 0 1.5em 1.5em; + } + + ul { + list-style: disc; + } + + th.sortable a, th.sorted a { + display: flex; + flex-direction: row; + } +} diff --git a/src/js/components/SnippetForm/fields/SnippetTypeInput.tsx b/src/js/components/SnippetForm/fields/SnippetTypeInput.tsx index 9891749d..4185d6a6 100644 --- a/src/js/components/SnippetForm/fields/SnippetTypeInput.tsx +++ b/src/js/components/SnippetForm/fields/SnippetTypeInput.tsx @@ -3,9 +3,9 @@ import classnames from 'classnames' import { __, _x } from '@wordpress/i18n' import Select from 'react-select' import { useSnippetForm } from '../../../hooks/useSnippetForm' -import { SNIPPET_TYPE_SCOPES } from '../../../types/Snippet' +import { SNIPPET_TYPE_SCOPES, SNIPPET_TYPES } from '../../../types/Snippet' import { isLicensed } from '../../../utils/screen' -import { getSnippetType, isProType } from '../../../utils/snippets/snippets' +import { getSnippetType, isProType, SNIPPET_TYPE_LABELS } from '../../../utils/snippets/snippets' import { Badge } from '../../common/Badge' import type { Dispatch, SetStateAction } from 'react' import type { SnippetCodeType, SnippetType } from '../../../types/Snippet' @@ -23,13 +23,9 @@ const EDITOR_MODES: Record = { html: 'application/x-httpd-php' } -const OPTIONS: SelectOption[] = [ - { value: 'php', label: __('Functions', 'code-snippets') }, - { value: 'html', label: __('Content', 'code-snippets') }, - { value: 'css', label: __('Styles', 'code-snippets') }, - { value: 'js', label: __('Scripts', 'code-snippets') }, - { value: 'cond', label: __('Conditions', 'code-snippets') } -] +const OPTIONS: SelectOption[] = + SNIPPET_TYPES.map(type => + ({ value: type, label: SNIPPET_TYPE_LABELS[type] })) const SnippetTypeOption: React.FC> = ({ label, value }) =>
diff --git a/src/js/components/SnippetsTable/SnippetTypeTabs.tsx b/src/js/components/SnippetsTable/SnippetTypeTabs.tsx new file mode 100644 index 00000000..68413e54 --- /dev/null +++ b/src/js/components/SnippetsTable/SnippetTypeTabs.tsx @@ -0,0 +1,65 @@ +import React, { MouseEventHandler } from 'react' +import classnames from 'classnames' +import { SNIPPET_TYPES, SnippetType } from '../../types/Snippet' +import { addQueryArgs } from '@wordpress/url' +import { SNIPPET_TYPE_LABELS } from '../../utils/snippets/snippets' +import { Badge } from '../common/Badge' +import { __ } from '@wordpress/i18n' + +const updateQueryParam = (name: string, value?: string) => { + if ('URLSearchParams' in window) { + const searchParams = new URLSearchParams(window.location.search) + + if (value) { + searchParams.set(name, value) + } else { + searchParams.delete(name) + } + + const newUrl = window.location.toString().replace(window.location.search, `?${searchParams}`) + console.log(window.location.search, searchParams.toString(), newUrl) + window.history.pushState({}, document.title, newUrl) + } +} + +interface TabProps { + type?: SnippetType + activeTab?: SnippetType + setActiveTab: (tab?: SnippetType) => void +} + +const Tab: React.FC = ({ type, activeTab, setActiveTab }) => { + const tabName = type ?? 'all' + + const handleClick: MouseEventHandler = event => { + event.preventDefault() + setActiveTab(type) + updateQueryParam('type', type) + } + + return ( + + + {type ? SNIPPET_TYPE_LABELS[type] : __('All Snippets', 'code-snippets')} + + {type && } + + ) +} + +export interface SnippetTypeTabsProps { + activeTab?: SnippetType + setActiveTab: (tab?: SnippetType) => void +} + +export const SnippetTypeTabs: React.FC = ({ activeTab, setActiveTab }) => +

+ + + {SNIPPET_TYPES.map(type => + )} +

diff --git a/src/js/components/SnippetsTable/SnippetsListTable.tsx b/src/js/components/SnippetsTable/SnippetsListTable.tsx new file mode 100644 index 00000000..192e5e0d --- /dev/null +++ b/src/js/components/SnippetsTable/SnippetsListTable.tsx @@ -0,0 +1,151 @@ +import { __, _x, sprintf } from '@wordpress/i18n' +import { useSnippetsList } from '../../hooks/useSnippetsList' +import { Snippet, SNIPPET_STATUSES, SnippetStatus, SnippetType } from '../../types/Snippet' +import { getSnippetType } from '../../utils/snippets/snippets' +import { ListTable, ListTableBulkAction } from '../common/ListTable' +import React, { useMemo, useState } from 'react' +import { addQueryArgs } from '@wordpress/url' +import { SubmitButton } from '../common/SubmitButton' +import { TableColumns } from './TableColumns' + +const VIEW_LABELS: Record = { + all: __('All', 'code-snippets'), + active: __('Active', 'code-snippets'), + inactive: __('Inactive', 'code-snippets'), + recently_activated: __('Recently Activate', 'code-snippets') +} + +const actions: ListTableBulkAction[] = [ + { + name: __('Activate', 'code-snippets'), + apply: () => Promise.resolve() + }, + { + name: __('Deactivate', 'code-snippets'), + apply: () => Promise.resolve() + }, + { + name: __('Clone', 'code-snippets'), + apply: () => Promise.resolve() + }, + { + name: __('Export', 'code-snippets'), + apply: () => Promise.resolve() + }, + { + name: __('Export code', 'code-snippets'), + apply: () => Promise.resolve() + }, + { + name: __('Delete', 'code-snippets'), + apply: () => Promise.resolve() + } +] + +interface TableViewsProps { + currentStatus?: SnippetStatus + setCurrentStatus: (status?: SnippetStatus) => void +} + +const TableViews: React.FC = ({ currentStatus, setCurrentStatus }) => { + + return ( + + ) +} + +interface ExtraTableNavProps { + which: 'top' | 'bottom' + currentTag?: string + currentStatus?: SnippetStatus + visibleSnippets: Snippet[] + setCurrentTag: (tag?: string) => void +} + +const ExtraTableNav: React.FC = ({ which, currentTag, setCurrentTag, currentStatus, visibleSnippets }) => { + + const tagsList: Set | undefined = useMemo( + () => + 'top' === which + ? visibleSnippets.reduce((tags, snippet) => { + snippet.tags.forEach(tag => tags.add(tag)) + return tags + }, new Set()) + : undefined, + [which, visibleSnippets]) + + return ( + <> + {tagsList ? +
+ +
+ : null} + + {'recently_activated' === currentStatus + ?
+ +
+ : null} + + ) +} + +export interface SnippetsListTableProps { + currentType?: SnippetType + currentStatus?: SnippetStatus + setCurrentStatus: (status?: SnippetStatus) => void +} + +export const SnippetsListTable: React.FC = ({ currentType, currentStatus, setCurrentStatus }) => { + const { snippetsList } = useSnippetsList() + const [currentTag, setCurrentTag] = useState() + + const visibleSnippets = useMemo( + () => snippetsList?.filter(snippet => + (!currentType || getSnippetType(snippet) === currentType) && + (!currentStatus || (currentStatus === 'active' && snippet.active) || (currentStatus === 'inactive' && !snippet.active)) && + (!currentTag || snippet.tags.includes(currentTag)) + ) ?? [], + [snippetsList, currentType, currentTag, currentStatus]) + + return ( + <> + + + snippet.id} + columns={TableColumns} + actions={actions} + extraTableNav={which => + } + /> + + ) +} diff --git a/src/js/components/SnippetsTable/SnippetsTable.tsx b/src/js/components/SnippetsTable/SnippetsTable.tsx index 71c97d6d..467415d3 100644 --- a/src/js/components/SnippetsTable/SnippetsTable.tsx +++ b/src/js/components/SnippetsTable/SnippetsTable.tsx @@ -1,4 +1,41 @@ -import React from "react" +import { __ } from "@wordpress/i18n" +import React, { useState } from "react" +import { WithRestAPIContext } from '../../hooks/useRestAPI' +import { WithSnippetsListContext } from '../../hooks/useSnippetsList' +import { SNIPPET_STATUSES, SNIPPET_TYPES, SnippetStatus, SnippetType } from '../../types/Snippet' +import { SnippetsListTable } from './SnippetsListTable' +import { SnippetTypeTabs } from './SnippetTypeTabs' + +const fetchQueryParam = (name: string): string | undefined => { + const urlParams = new URLSearchParams(window.location.search) + return urlParams.get(name) ?? undefined +} + +const Page = () => { + const [currentType, setCurrentType] = useState(() => { + const type = fetchQueryParam('type') + return type && SNIPPET_TYPES.includes(type as SnippetType) ? (type as SnippetType) : undefined + }) + + const [currentStatus, setCurrentStatus] = useState(() => { + const status = fetchQueryParam('status') + return status && SNIPPET_STATUSES.includes(status as SnippetStatus) ? (status as SnippetStatus) : undefined + }) + + return ( +
+

{__('Manage Code Snippets', 'code-snippets')}

+ + + + +
+ ) +} export const SnippetsTable: React.FC = () => -
TODO
+ + + + + diff --git a/src/js/components/SnippetsTable/TableColumns.tsx b/src/js/components/SnippetsTable/TableColumns.tsx new file mode 100644 index 00000000..a676f6fa --- /dev/null +++ b/src/js/components/SnippetsTable/TableColumns.tsx @@ -0,0 +1,117 @@ +import React, { ChangeEventHandler, useState } from 'react' +import { useRestAPI } from '../../hooks/useRestAPI' +import { useSnippetsList } from '../../hooks/useSnippetsList' +import { Snippet } from '../../types/Snippet' +import { handleUnknownError } from '../../utils/errors' +import { getSnippetType } from '../../utils/snippets/snippets' +import { stripTags } from '../../utils/text' +import { Badge } from '../common/Badge' +import { ListTableColumn } from '../common/ListTable' +import { __ } from '@wordpress/i18n' +import { addQueryArgs } from '@wordpress/url' +import { humanTimeDiff } from '@wordpress/date' + +interface ColumnProps { + snippet: Snippet +} + +const ActivateColumn: React.FC = ({ snippet }) => { + const { snippetsAPI: { activate, deactivate } } = useRestAPI() + const { refreshSnippetsList } = useSnippetsList() + + return ( + { + (snippet.active ? deactivate(snippet) : activate(snippet)) + .then(() => refreshSnippetsList()) + .catch(handleUnknownError) + }} + /> + ) +} + +const NameColumn: React.FC = ({ snippet }) => { + return snippet.name +} + +const PriorityColumn: React.FC = ({ snippet }) => { + const [value, setValue] = useState(snippet.priority) + // const { snippetsAPI: { update } } = useRestAPI() + const id = `snippet-${snippet.id}-priority` + + const handleUpdate: ChangeEventHandler = event => { + console.log(event, value) + } + + return ( + <> + + setValue(Number(event.target.value))} + /> + + ) +} + +export const TableColumns: ListTableColumn[] = [ + { + id: 'activate', + render: snippet => + }, + { + id: 'name', + title: __('Name', 'code-snippets'), + isPrimary: true, + sortedValue: item => item.name.toLowerCase(), + render: snippet => + }, + { + id: 'type', + title: __('Type', 'code-snippets'), + sortedValue: item => getSnippetType(item), + render: snippet => + }, + { + id: 'desc', + title: __('Description', 'code-snippets'), + // TODO: figure out how to allow formatting and markup. + render: snippet => stripTags(snippet.desc) + }, + { + id: 'tags', + title: __('Tags', 'code-snippets'), + render: snippet => + snippet.tags.map((tag, index) => + <> + + {tag} + + {index < snippet.tags.length - 1 ? ', ' : ''} + + ) + }, + { + id: 'date', + title: __('Modified', 'code-snippets'), + sortedValue: snippet => snippet.modified ? new Date(snippet.modified).toISOString() : '', + render: snippet => snippet.modified ? humanTimeDiff(snippet.modified, undefined) : '—' + }, + { + id: 'priority', + title: __('Priority', 'code-snippets'), + sortedValue: snippet => snippet.priority, + render: snippet => + } +] diff --git a/src/js/components/common/ListTable/ListTable.tsx b/src/js/components/common/ListTable/ListTable.tsx new file mode 100644 index 00000000..14e1a029 --- /dev/null +++ b/src/js/components/common/ListTable/ListTable.tsx @@ -0,0 +1,119 @@ +import React, { useMemo, useState } from 'react' +import classnames from 'classnames' +import { TableHeadings } from './TableHeadings' +import { TableItems } from './TableItems' +import { TableNav } from './TableNav' +import type { TableNavProps } from './TableNav' +import type { TableHeadingsProps } from './TableHeadings' +import type { Key, ReactNode } from 'react' + +export interface ListTableColumn { + id: Key + title?: ReactNode + render: (item: T) => ReactNode + isHidden?: boolean + isPrimary?: boolean + sortedValue?: (item: T) => Key + defaultSortDirection?: ListTableSortDirection +} + +export interface ListTableBulkAction { + name: string + apply: (selected: Set) => Promise +} + +export interface ListTableBulkActionGroup { + name: string + actions: ListTableBulkAction[] +} + +export type ListTableSortDirection = 'asc' | 'desc' + +export interface ListTableNavProps { + actions?: readonly (ListTableBulkAction | ListTableBulkActionGroup)[] + isDisabled?: boolean + extraTableNav?: (which: 'top' | 'bottom') => ReactNode +} + +export interface ListTableItemsProps { + items: T[] + getKey: (item: T) => K + columns: ListTableColumn[] + noItems?: ReactNode + rowClassName?: (item: T) => string +} + +export interface ListTableProps extends ListTableItemsProps, ListTableNavProps { + fixed?: boolean + striped?: boolean + className?: string +} + +const sortItems = ( + items: T[], + sortColumn: ListTableColumn | undefined, + sortDirection: ListTableSortDirection +): T[] => + items.toSorted((itemA, itemB) => { + const valueA = sortColumn?.sortedValue?.(itemA) + const valueB = sortColumn?.sortedValue?.(itemB) + + if (valueA === undefined || valueB === undefined) { + return 0 + } + + if (valueA < valueB) { + return 'asc' === sortDirection ? -1 : 1 + } + + if (valueA > valueB) { + return 'asc' === sortDirection ? 1 : -1 + } + + return 0 + }) + +export const ListTable = ({ + items, + fixed, + striped, + getKey, + columns, + actions, + noItems, + className, + extraTableNav, + isDisabled = false, +}: ListTableProps) => { + const [selected, setSelected] = useState(new Set()) + const [sortColumn, setSortColumn] = useState>() + const [sortDirection, setSortDirection] = useState('asc') + + const sortedItems: T[] = useMemo( + () => sortItems(items, sortColumn, sortDirection), + [items, sortColumn, sortDirection]) + + const tableNavProps: Omit, 'which'> = + { hasItems: 0 < items.length, actions, extraTableNav, selected, isDisabled } + + const tableHeadingsProps: Omit, 'which'> = + { items: sortedItems, setSelected, columns, getKey, sortColumn, setSortColumn, sortDirection, setSortDirection } + + return ( + <> + + + + + + + + + + + +
+ + + ) +} diff --git a/src/js/components/common/ListTable/TableHeadings.tsx b/src/js/components/common/ListTable/TableHeadings.tsx new file mode 100644 index 00000000..55cbf909 --- /dev/null +++ b/src/js/components/common/ListTable/TableHeadings.tsx @@ -0,0 +1,102 @@ +import React from 'react' +import classnames from 'classnames' +import { __ } from '@wordpress/i18n' +import type { ListTableColumn, ListTableProps, ListTableSortDirection } from './ListTable' +import type { Dispatch, Key, SetStateAction, ThHTMLAttributes } from 'react' + +interface SortableHeadingProps { + column: ListTableColumn + cellProps: ThHTMLAttributes + sortColumn: ListTableColumn | undefined + sortDirection: ListTableSortDirection + setSortColumn: Dispatch | undefined>> + setSortDirection: Dispatch> +} + +const SortableHeading = ({ + column, + cellProps, + sortColumn, + setSortColumn, + sortDirection, + setSortDirection +}: SortableHeadingProps) => { + const isCurrent = column.id === sortColumn?.id + + const newSortDirection = isCurrent + ? 'asc' === sortDirection ? 'desc' : 'asc' + : column.defaultSortDirection ?? 'asc' + + return ( + + { + event.preventDefault() + setSortColumn(column) + setSortDirection(newSortDirection) + }}> + {column.title} + + + + + {isCurrent ? null + : + {/* translators: Hidden accessibility text. */} + {'asc' === newSortDirection ? __('Sort ascending.', 'code-snippets') : __('Sort descending.', 'code-snippets')} + } + + + ) +} + +export interface TableHeadingsProps extends Pick, 'columns' | 'getKey' | 'items'> { + which: 'head' | 'foot' + sortColumn: ListTableColumn | undefined + setSelected: Dispatch>> + sortDirection: ListTableSortDirection + setSortColumn: Dispatch | undefined>> + setSortDirection: Dispatch> +} + +export const TableHeadings = ({ + items, + which, + getKey, + columns, + sortColumn, + setSelected, + setSortColumn, + sortDirection, + setSortDirection +}: TableHeadingsProps) => + + + { + setSelected(new Set(event.target.checked ? items.map(getKey) : null)) + }} + /> + + + {columns.map(column => { + const cellProps: ThHTMLAttributes = { + id: 'head' === which ? column.id.toString() : undefined, + scope: 'col', + className: classnames( + 'manage-column', + `column-${column.id}`, + `${column.id}-column`, + { 'hidden': column.isHidden, 'column-primary': column.isPrimary } + ) + } + + return column.sortedValue + ? + : {column.title} + })} + diff --git a/src/js/components/common/ListTable/TableItems.tsx b/src/js/components/common/ListTable/TableItems.tsx new file mode 100644 index 00000000..c5b0996f --- /dev/null +++ b/src/js/components/common/ListTable/TableItems.tsx @@ -0,0 +1,62 @@ +import React from 'react' +import type { Dispatch, Key, SetStateAction, ThHTMLAttributes } from 'react' +import type { ListTableColumn, ListTableItemsProps } from './ListTable' + +interface CheckboxCellProps extends Pick, 'getKey'> { + item: T + setSelected: Dispatch>> +} + +const CheckboxCell = ({ item, setSelected, getKey }: CheckboxCellProps) => + + { + setSelected(previous => { + const updated = new Set(previous) + + if (event.target.checked) { + updated.add(getKey(item)) + } else { + updated.delete(getKey(item)) + } + + return updated + }) + }} + /> + + +interface TableCellProps { + item: T + column: ListTableColumn +} + +const TableCell = ({ item, column }: TableCellProps) => { + const props: ThHTMLAttributes = { + className: `${column.id}-column column-${column.id}`, + children: column.render(item) + } + + return column.isPrimary ? : +} + +export interface TableItemsProps + extends Pick, 'items' | 'getKey' | 'columns' | 'noItems' | 'rowClassName'> { + setSelected: Dispatch>> +} + +export const TableItems = ({ items, getKey, columns, noItems, setSelected, rowClassName }: TableItemsProps) => + 0 < items.length + ? items.map(item => + + + + {columns.map(column => + )} + + ) + : + {noItems} + diff --git a/src/js/components/common/ListTable/TableNav.tsx b/src/js/components/common/ListTable/TableNav.tsx new file mode 100644 index 00000000..89a0b59d --- /dev/null +++ b/src/js/components/common/ListTable/TableNav.tsx @@ -0,0 +1,116 @@ +import React, { useMemo, useState } from 'react' +import { __ } from '@wordpress/i18n' +import { Spinner } from '@wordpress/components' +import { handleUnknownError } from '../../../utils/errors' +import { SubmitButton } from '../SubmitButton' +import type { ListTableBulkAction, ListTableNavProps } from './ListTable' +import type { Key } from 'react' + +interface BulkActionSelectProps extends Required, 'which' | 'actions'>> { + setSelectedAction: (action: ListTableBulkAction | undefined) => void +} + +const BulkActionSelect = ({ which, actions, setSelectedAction }: BulkActionSelectProps) => { + const actionsMap: Map> = useMemo( + () => new Map( + actions + .flatMap(actionOrGroup => + 'actions' in actionOrGroup ? actionOrGroup.actions : [actionOrGroup]) + .map(action => [action.name, action]) + ), [actions]) + + return ( + + ) +} + +interface BulkActionsProps extends Required, 'which' | 'actions'>> { + applyAction: (action: ListTableBulkAction) => Promise +} + +const BulkActions = ({ which, actions, applyAction }: BulkActionsProps) => { + const [selectedAction, setSelectedAction] = useState>() + const [isPerformingAction, setIsPerformingAction] = useState(false) + + return ( +
+ + + + + { + event.preventDefault() + + if (selectedAction) { + setIsPerformingAction(true) + applyAction(selectedAction) + .catch(handleUnknownError) + .finally(() => { + setSelectedAction(undefined) + setIsPerformingAction(false) + }) + } + }} + /> + + {isPerformingAction ? : null} +
+ ) +} + +export interface TableNavProps extends ListTableNavProps { + which: 'top' | 'bottom' + hasItems: boolean + selected: Set +} + +export const TableNav = ({ + which, + actions, + hasItems, + selected, + extraTableNav +}: TableNavProps) => + extraTableNav || hasItems && actions + ?
+ + {hasItems && actions + ? action.apply(selected)} + /> + : null} + + {extraTableNav?.(which)} + + {/* TODO pagination */} + +
+
+ : null diff --git a/src/js/components/common/ListTable/index.ts b/src/js/components/common/ListTable/index.ts new file mode 100644 index 00000000..d862c206 --- /dev/null +++ b/src/js/components/common/ListTable/index.ts @@ -0,0 +1 @@ +export * from './ListTable' diff --git a/src/js/types/Snippet.ts b/src/js/types/Snippet.ts index dfdec93f..948826ef 100644 --- a/src/js/types/Snippet.ts +++ b/src/js/types/Snippet.ts @@ -14,8 +14,13 @@ export interface Snippet { readonly code_error?: readonly [string, number] | null } +export const SNIPPET_TYPES = ['php', 'html', 'css', 'js', 'cond'] as const +export const SNIPPET_STATUSES = ['active', 'inactive', 'recently_activated'] as const + +export type SnippetType = typeof SNIPPET_TYPES[number] +export type SnippetStatus = typeof SNIPPET_STATUSES[number] + export type SnippetCodeType = 'php' | 'html' | 'css' | 'js' -export type SnippetType = SnippetCodeType | 'cond' export type SnippetCodeScope = typeof SNIPPET_TYPE_SCOPES[SnippetCodeType][number] export type SnippetScope = typeof SNIPPET_TYPE_SCOPES[SnippetType][number] diff --git a/src/js/utils/snippets/snippets.ts b/src/js/utils/snippets/snippets.ts index 20a0fd74..4a3a5d20 100644 --- a/src/js/utils/snippets/snippets.ts +++ b/src/js/utils/snippets/snippets.ts @@ -3,6 +3,14 @@ import { isNetworkAdmin } from '../screen' import { parseSnippetObject } from './objects' import type { Snippet, SnippetType } from '../../types/Snippet' +export const SNIPPET_TYPE_LABELS: Record = { + php: __('Functions', 'code-snippets'), + html: __('Content', 'code-snippets'), + css: __('Styles', 'code-snippets'), + js: __('Scripts', 'code-snippets'), + cond: __('Conditions', 'code-snippets') +} + const PRO_TYPES = new Set(['css', 'js', 'cond']) const defaults: Omit = { diff --git a/src/php/admin-menus/class-manage-menu.php b/src/php/admin-menus/class-manage-menu.php index 17dd318d..7b711820 100644 --- a/src/php/admin-menus/class-manage-menu.php +++ b/src/php/admin-menus/class-manage-menu.php @@ -170,6 +170,15 @@ public function load() { $contextual_help = new Contextual_Help( 'edit' ); $contextual_help->load(); + + add_screen_option( + 'per_page', + array( + 'label' => __( 'Snippets per page', 'code-snippets' ), + 'default' => 999, + 'option' => 'snippets_per_page', + ) + ); } /** @@ -196,6 +205,7 @@ public function enqueue_assets() { 'react-dom', 'wp-url', 'wp-i18n', + 'wp-date', 'wp-components', ], $plugin->version, From d1a4718790cc3bb31c21b2568f091d3f9ec32c62 Mon Sep 17 00:00:00 2001 From: Shea Bunge Date: Fri, 25 Jul 2025 00:55:21 +1000 Subject: [PATCH 047/318] Improve look and function of React snippets table. --- src/css/manage.scss | 102 +++++++-- .../EditorSidebar/actions/ExportButtons.tsx | 19 +- src/js/components/SnippetForm/SnippetForm.tsx | 16 +- .../SnippetForm/fields/SnippetTypeInput.tsx | 13 +- .../SnippetForm/page/PageHeading.tsx | 7 +- .../SnippetsTable/SnippetTypeTabs.tsx | 65 ------ .../SnippetsTable/SnippetsListTable.tsx | 212 +++++++++++------- .../SnippetsTable/SnippetsTable.tsx | 97 ++++++-- .../components/SnippetsTable/TableColumns.tsx | 137 +++++++++-- .../components/common/ListTable/ListTable.tsx | 4 +- .../common/ListTable/TableItems.tsx | 11 +- src/js/hooks/useRestAPI.tsx | 16 +- src/js/hooks/useSnippetForm.tsx | 2 +- src/js/hooks/useSnippetSubmit.ts | 85 ------- src/js/hooks/useSnippetsAPI.ts | 10 +- src/js/hooks/useSnippetsList.tsx | 2 +- src/js/hooks/useSnippetsTable.tsx | 170 ++++++++++++++ src/js/types/Snippet.ts | 5 +- src/js/types/Window.ts | 4 + src/js/types/schema/SnippetSchema.ts | 1 + src/js/utils/bootstrap.tsx | 23 +- src/js/utils/files.ts | 17 +- src/js/utils/restAPI.ts | 2 + src/js/utils/snippets/objects.ts | 61 +++-- src/js/utils/snippets/snippets.ts | 23 +- src/php/admin-menus/class-edit-menu.php | 2 +- src/php/admin-menus/class-manage-menu.php | 9 +- src/php/class-plugin.php | 12 +- src/php/class-snippet.php | 12 + src/php/rest-api/class-rest-api.php | 129 +++++++++++ .../class-snippets-rest-controller.php | 5 + src/php/snippet-ops.php | 30 ++- 32 files changed, 894 insertions(+), 409 deletions(-) delete mode 100644 src/js/components/SnippetsTable/SnippetTypeTabs.tsx delete mode 100644 src/js/hooks/useSnippetSubmit.ts create mode 100644 src/js/hooks/useSnippetsTable.tsx create mode 100644 src/php/rest-api/class-rest-api.php diff --git a/src/css/manage.scss b/src/css/manage.scss index 836280ce..e55346f9 100644 --- a/src/css/manage.scss +++ b/src/css/manage.scss @@ -2,21 +2,14 @@ @use 'common/switch'; @use 'common/select'; -.wrap h2.nav-tab-wrapper { - display: flex; - flex-flow: row wrap-reverse; - gap: 0.5em; -} - .nav-tab { display: flex; flex-flow: row wrap; align-items: center; gap: 8px; - margin: 0; @media screen and (width <= 1190px) { - .snippet-label { + span:first-child:not(:last-child) { display: none; } } @@ -26,13 +19,31 @@ margin: 10px 0 0; } -.wrap .subsubsub { - li::after { - content: ' | '; +.name-column, +.type-column { + .dashicons { + font-size: 16px; + inline-size: 16px; + block-size: 16px; + vertical-align: middle; + } + + .dashicons-clock { + vertical-align: middle; + } +} + +.active-snippet .name-column > a { + font-weight: 600; +} + +.active-snippet { + td, th { + background-color: rgba(#78c8e6, 0.06); } - li:last-child::after { - content: ''; + th.check-column { + border-inline-start: 2px solid #2ea2cc; } } @@ -78,4 +89,69 @@ display: flex; flex-direction: row; } + + .row-actions { + color: #ddd; + position: relative; + inset-inline-start: 0; + + .button-link { + min-height: unset; + line-height: unset; + + &:hover { + background: none; + } + } + } + + .column-activate { + padding-inline-end: 0 !important; + } + + .clear-filters { + vertical-align: middle; + } + + tfoot th.check-column { + padding: 13px 0 0 3px; + } + + thead th.check-column, + tfoot th.check-column, + .inactive-snippet th.check-column { + padding-inline-start: 5px; + } + + .active-snippet, .inactive-snippet { + td, th { + padding: 10px 9px; + border: none; + box-shadow: inset 0 -1px 0 rgb(0 0 0 / 10%); + } + } + + tr.active-snippet + tr.inactive-snippet th, + tr.active-snippet + tr.inactive-snippet td { + border-block-start: 1px solid rgb(0 0 0 / 3%); + box-shadow: inset 0 1px 0 rgb(0 0 0 / 2%), inset 0 -1px 0 #e1e1e1; + } + + .delete { + color: #b32d2e; + + &:hover { + border-block-end: 1px solid #f00; + color: #f00; + } + } + + #wpbody-content & .column-name { + white-space: nowrap; /* prevents wrapping of snippet title */ + vertical-align: top; + } +} + +.wp-core-ui .button.clear-filters { + vertical-align: baseline; } diff --git a/src/js/components/EditorSidebar/actions/ExportButtons.tsx b/src/js/components/EditorSidebar/actions/ExportButtons.tsx index 466256db..9d4cc8a0 100644 --- a/src/js/components/EditorSidebar/actions/ExportButtons.tsx +++ b/src/js/components/EditorSidebar/actions/ExportButtons.tsx @@ -1,27 +1,14 @@ import React from 'react' import { __ } from '@wordpress/i18n' import { useRestAPI } from '../../../hooks/useRestAPI' -import { Button } from '../../common/Button' import { downloadSnippetExportFile } from '../../../utils/files' +import { Button } from '../../common/Button' import { useSnippetForm } from '../../../hooks/useSnippetForm' -import type { SnippetsExport } from '../../../types/schema/SnippetsExport' export const ExportButtons: React.FC = () => { const { snippetsAPI } = useRestAPI() const { snippet, isWorking, setIsWorking, handleRequestError } = useSnippetForm() - const handleFileResponse = (response: string | SnippetsExport) => { - setIsWorking(false) - console.info('file response', response) - - if ('string' === typeof response) { - downloadSnippetExportFile(response, snippet) - } else { - const JSON_INDENT_SPACES = 2 - downloadSnippetExportFile(JSON.stringify(response, undefined, JSON_INDENT_SPACES), snippet, 'json') - } - } - return (
+ + : null} + ) } +const SnippetsTableInner = () => +
+ + +

+ + {SNIPPET_TYPES.map(type => )} +

+ + +
+ export const SnippetsTable: React.FC = () => - + + + diff --git a/src/js/components/SnippetsTable/TableColumns.tsx b/src/js/components/SnippetsTable/TableColumns.tsx index a676f6fa..14216dea 100644 --- a/src/js/components/SnippetsTable/TableColumns.tsx +++ b/src/js/components/SnippetsTable/TableColumns.tsx @@ -1,15 +1,19 @@ -import React, { ChangeEventHandler, useState } from 'react' +import React, { Fragment, useState } from 'react' +import { __, sprintf } from '@wordpress/i18n' +import { addQueryArgs } from '@wordpress/url' +import { humanTimeDiff } from '@wordpress/date' +import { Modal } from '@wordpress/components' import { useRestAPI } from '../../hooks/useRestAPI' import { useSnippetsList } from '../../hooks/useSnippetsList' -import { Snippet } from '../../types/Snippet' import { handleUnknownError } from '../../utils/errors' -import { getSnippetType } from '../../utils/snippets/snippets' +import { downloadSnippetExportFile } from '../../utils/files' +import { isNetworkAdmin } from '../../utils/screen' +import { getSnippetEditUrl, getSnippetType } from '../../utils/snippets/snippets' import { stripTags } from '../../utils/text' import { Badge } from '../common/Badge' -import { ListTableColumn } from '../common/ListTable' -import { __ } from '@wordpress/i18n' -import { addQueryArgs } from '@wordpress/url' -import { humanTimeDiff } from '@wordpress/date' +import { Button } from '../common/Button' +import type { Snippet } from '../../types/Snippet' +import type { ListTableColumn } from '../common/ListTable' interface ColumnProps { snippet: Snippet @@ -36,32 +40,129 @@ const ActivateColumn: React.FC = ({ snippet }) => { ) } +const DeleteRowAction: React.FC = ({ snippet }) => { + const { snippetsAPI } = useRestAPI() + const [confirmDeleteDialogOpen, setConfirmDeleteDialogOpen] = useState(false) + const { refreshSnippetsList } = useSnippetsList() + + return ( + <> + + + {confirmDeleteDialogOpen + ? setConfirmDeleteDialogOpen(false)} + closeButtonLabel={__('Cancel', 'code-snippets')} + > + {__('You are about to permanently delete this snippet.', 'code-snippets')} + + + + + + : null} + + ) +} + +const RowActions: React.FC = ({ snippet }) => { + const { snippetsAPI } = useRestAPI() + + if (!isNetworkAdmin() && snippet.network && !snippet.shared_network) { + return ( +
+ {snippet.active + ? {__('Network Active', 'code-snippets')} + : {__('Network Only', 'code-snippets')}} +
+ ) + } + + if (snippet.shared_network && !window.CODE_SNIPPETS_MANAGE?.hasNetworkCap) { + return undefined + } + + return ( +
+ {__('Edit', 'code-snippets')}{' | '} + + {' | '} + + +
+ ) +} + const NameColumn: React.FC = ({ snippet }) => { - return snippet.name + // translators: %s: snippet identifier. + const displayName = snippet.name.trim() ? snippet.name : sprintf(__('Snippet #%d', 'code-snippets'), snippet.id) + + return ( + <> + {isNetworkAdmin() || !snippet.network || window.CODE_SNIPPETS_MANAGE?.hasNetworkCap + ? {displayName} + : displayName} + + {snippet.shared_network && {__('Shared on Network', 'code-snippets')}} + + + + ) } const PriorityColumn: React.FC = ({ snippet }) => { const [value, setValue] = useState(snippet.priority) - // const { snippetsAPI: { update } } = useRestAPI() + const { snippetsAPI } = useRestAPI() + const { refreshSnippetsList } = useSnippetsList() const id = `snippet-${snippet.id}-priority` - const handleUpdate: ChangeEventHandler = event => { - console.log(event, value) + const handleUpdate = () => { + snippetsAPI.update({ ...snippet, priority: value }) + .then(response => { + if (response.id === snippet.id) { + setValue(response.priority) + } + }) + .then(refreshSnippetsList) + .catch(handleUnknownError) } return ( - <> +
{ + event.preventDefault() + handleUpdate() + }}> setValue(Number(event.target.value))} /> - +
) } @@ -94,19 +195,21 @@ export const TableColumns: ListTableColumn[] = [ title: __('Tags', 'code-snippets'), render: snippet => snippet.tags.map((tag, index) => - <> + {tag} {index < snippet.tags.length - 1 ? ', ' : ''} - + ) }, { id: 'date', title: __('Modified', 'code-snippets'), sortedValue: snippet => snippet.modified ? new Date(snippet.modified).toISOString() : '', - render: snippet => snippet.modified ? humanTimeDiff(snippet.modified, undefined) : '—' + render: snippet => snippet.modified + ? + : '—' }, { id: 'priority', diff --git a/src/js/components/common/ListTable/ListTable.tsx b/src/js/components/common/ListTable/ListTable.tsx index 14e1a029..9e58fb96 100644 --- a/src/js/components/common/ListTable/ListTable.tsx +++ b/src/js/components/common/ListTable/ListTable.tsx @@ -13,6 +13,7 @@ export interface ListTableColumn { render: (item: T) => ReactNode isHidden?: boolean isPrimary?: boolean + isHeading?: boolean sortedValue?: (item: T) => Key defaultSortDirection?: ListTableSortDirection } @@ -82,6 +83,7 @@ export const ListTable = ({ actions, noItems, className, + rowClassName, extraTableNav, isDisabled = false, }: ListTableProps) => { @@ -107,7 +109,7 @@ export const ListTable = ({ - + diff --git a/src/js/components/common/ListTable/TableItems.tsx b/src/js/components/common/ListTable/TableItems.tsx index c5b0996f..7fd3ea00 100644 --- a/src/js/components/common/ListTable/TableItems.tsx +++ b/src/js/components/common/ListTable/TableItems.tsx @@ -1,5 +1,5 @@ import React from 'react' -import type { Dispatch, Key, SetStateAction, ThHTMLAttributes } from 'react' +import type { Dispatch, Key, SetStateAction } from 'react' import type { ListTableColumn, ListTableItemsProps } from './ListTable' interface CheckboxCellProps extends Pick, 'getKey'> { @@ -34,12 +34,11 @@ interface TableCellProps { } const TableCell = ({ item, column }: TableCellProps) => { - const props: ThHTMLAttributes = { - className: `${column.id}-column column-${column.id}`, - children: column.render(item) - } + const className = `${column.id}-column column-${column.id}` - return column.isPrimary ? : + return column.isHeading + ? {column.render(item)} + : {column.render(item)} } export interface TableItemsProps diff --git a/src/js/hooks/useRestAPI.tsx b/src/js/hooks/useRestAPI.tsx index ece0ef5f..12314e5b 100644 --- a/src/js/hooks/useRestAPI.tsx +++ b/src/js/hooks/useRestAPI.tsx @@ -5,7 +5,7 @@ import { REST_API_AXIOS_CONFIG } from '../utils/restAPI' import { buildSnippetsAPI } from '../utils/snippets/api' import type { SnippetsAPI } from '../utils/snippets/api' import type { PropsWithChildren } from 'react' -import type { AxiosInstance, AxiosResponse } from 'axios' +import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios' export interface RestAPIContext { api: RestAPI @@ -15,8 +15,8 @@ export interface RestAPIContext { export interface RestAPI { get: (url: string) => Promise - post: (url: string, data?: D) => Promise - put: (url: string, data?: D) => Promise + post: (url: string, data?: D, config?: AxiosRequestConfig) => Promise + put: (url: string, data?: D, config?: AxiosRequestConfig) => Promise del: (url: string) => Promise } @@ -40,17 +40,17 @@ const buildRestAPI = (axiosInstance: AxiosInstance): RestAPI => ({ get: (url: string): Promise => debugRequest('GET', url, axiosInstance.get, never>(url)), - post: (url: string, data?: D): Promise => - debugRequest('POST', url, axiosInstance.post, typeof data>(url, data), data), + post: (url: string, data?: D, config?: AxiosRequestConfig): Promise => + debugRequest('POST', url, axiosInstance.post, D>(url, data, config), data), del: (url: string): Promise => debugRequest('DELETE', url, axiosInstance.delete, never>(url)), - put: (url: string, data?: D): Promise => - debugRequest('PUT', url, axiosInstance.put, typeof data>(url, data), data) + put: (url: string, data?: D, config?: AxiosRequestConfig): Promise => + debugRequest('PUT', url, axiosInstance.put, D>(url, data, config), data) }) -export const [RestAPIContext, useRestAPI] = createContextHook('RestAPI') +export const [RestAPIContext, useRestAPI] = createContextHook('useRestAPI') export const WithRestAPIContext: React.FC = ({ children }) => { const axiosInstance = useMemo(() => axios.create(REST_API_AXIOS_CONFIG), []) diff --git a/src/js/hooks/useSnippetForm.tsx b/src/js/hooks/useSnippetForm.tsx index 645d5a0f..62c9ec57 100644 --- a/src/js/hooks/useSnippetForm.tsx +++ b/src/js/hooks/useSnippetForm.tsx @@ -22,7 +22,7 @@ export interface SnippetFormContext { setCodeEditorInstance: Dispatch> } -export const [SnippetFormContext, useSnippetForm] = createContextHook('SnippetForm') +export const [SnippetFormContext, useSnippetForm] = createContextHook('useSnippetForm') export interface WithSnippetFormContextProps extends PropsWithChildren { initialSnippet: () => Snippet diff --git a/src/js/hooks/useSnippetSubmit.ts b/src/js/hooks/useSnippetSubmit.ts deleted file mode 100644 index bbc9e2ff..00000000 --- a/src/js/hooks/useSnippetSubmit.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { __ } from '@wordpress/i18n' -import { addQueryArgs } from '@wordpress/url' -import { isAxiosError } from 'axios' -import { useCallback } from 'react' -import { createSnippetObject } from '../utils/snippets' -import { useSnippetsAPI } from './useSnippetsAPI' -import type { Dispatch, SetStateAction } from 'react' -import type { ScreenNotice } from '../types/ScreenNotice' -import type { Snippet } from '../types/Snippet' - -const messages = { - addNew: __('Add New Snippet', 'code-snippets'), - edit: __('Edit Snippet', 'code-snippets'), - created: __('Snippet created.', 'code-snippets'), - updated: __('Snippet updated.', 'code-snippets'), - createdActivated: __('Snippet created and activated.', 'code-snippets'), - updatedActivated: __('Snippet updated and activated.', 'code-snippets'), - updatedExecuted: __('Snippet updated and executed.', 'code-snippets'), - updatedDeactivated: __('Snippet updated and deactivated'), - failedCreate: __('Could not create snippet.', 'code-snippets'), - failedUpdate: __('Could not update snippet.', 'code-snippets') -} - -const getSuccessNotice = (request: Snippet, response: Snippet, active: boolean | undefined): string => { - if (active === undefined) { - return 0 === request.id ? messages.created : messages.updated - } - - if (0 === request.id && active) { - return messages.createdActivated - } - - if (active) { - return 'single-use' === response.scope - ? messages.updatedExecuted - : messages.updatedActivated - } else { - return messages.updatedDeactivated - } -} - -export const useSnippetSubmit = ( - setSnippet: Dispatch>, - setIsWorking: Dispatch>, - setCurrentNotice: Dispatch> -): (snippet: Snippet, active?: boolean) => Promise => { - const api = useSnippetsAPI() - - return useCallback(async (snippet: Snippet, active?: boolean) => { - setIsWorking(true) - setCurrentNotice(undefined) - - const result = await (async (): Promise => { - try { - const requestData: Snippet = { ...snippet, active: active ?? snippet.active } - const data = await (0 === snippet.id ? api.create(requestData) : api.update(requestData)) - setIsWorking(false) - return data.id ? data : undefined - } catch (error) { - setIsWorking(false) - return isAxiosError(error) ? error.message : undefined - } - })() - - if (undefined === result || 'string' === typeof result) { - const message = [ - snippet.id ? messages.failedCreate : messages.failedUpdate, - result ?? __('The server did not send a valid response.', 'code-snippets') - ] - - setCurrentNotice(['error', message.filter(Boolean).join(' ')]) - return undefined - } else { - setSnippet(createSnippetObject(result)) - setCurrentNotice(['updated', getSuccessNotice(snippet, result, active)]) - - if (snippet.id && result.id) { - window.document.title = window.document.title.replace(messages.addNew, messages.edit) - window.history.replaceState({}, '', addQueryArgs(window.CODE_SNIPPETS?.urls.edit, { id: result.id })) - } - - return result - } - }, [api, setCurrentNotice, setIsWorking, setSnippet]) -} diff --git a/src/js/hooks/useSnippetsAPI.ts b/src/js/hooks/useSnippetsAPI.ts index 714e1e4e..5805e35a 100644 --- a/src/js/hooks/useSnippetsAPI.ts +++ b/src/js/hooks/useSnippetsAPI.ts @@ -3,7 +3,7 @@ import { addQueryArgs } from '@wordpress/url' import { handleUnknownError } from '../utils/errors' import { REST_API_AXIOS_CONFIG, REST_SNIPPETS_BASE } from '../utils/restAPI' import { isNetworkAdmin } from '../utils/screen' -import { createSnippetObject } from '../utils/snippets' +import { createSnippetObject } from '../utils/snippets/snippets' import { useAxios } from './useAxios' import type { Snippet } from '../types/Snippet' import type { SnippetsExport } from '../types/SnippetsExport' @@ -37,19 +37,19 @@ export const useSnippetsAPI = (): SnippetsAPI => { get(addQueryArgs(`${REST_SNIPPETS_BASE}/${snippetId}`, { network })), create: snippet => - post(REST_SNIPPETS_BASE, snippet), + post(REST_SNIPPETS_BASE, snippet), update: snippet => - post(buildURL(snippet), snippet), + post(buildURL(snippet), snippet), delete: (snippet: Snippet) => del(buildURL(snippet)), activate: snippet => - post(buildURL(snippet, 'activate')), + post(buildURL(snippet, 'activate')), deactivate: snippet => - post(buildURL(snippet, 'deactivate')), + post(buildURL(snippet, 'deactivate')), export: snippet => get(buildURL(snippet, 'export')), diff --git a/src/js/hooks/useSnippetsList.tsx b/src/js/hooks/useSnippetsList.tsx index f65c6566..3b18b5c4 100644 --- a/src/js/hooks/useSnippetsList.tsx +++ b/src/js/hooks/useSnippetsList.tsx @@ -10,7 +10,7 @@ export interface SnippetsListContext { refreshSnippetsList: () => Promise } -const [SnippetsListContext, useSnippetsList] = createContextHook('SnippetsList') +const [SnippetsListContext, useSnippetsList] = createContextHook('useSnippetsList') export const WithSnippetsListContext: React.FC = ({ children }) => { const { snippetsAPI: { fetchAll } } = useRestAPI() diff --git a/src/js/hooks/useSnippetsTable.tsx b/src/js/hooks/useSnippetsTable.tsx new file mode 100644 index 00000000..9b17339d --- /dev/null +++ b/src/js/hooks/useSnippetsTable.tsx @@ -0,0 +1,170 @@ +import React, { useCallback, useMemo, useState } from 'react' +import { SNIPPET_STATUSES, SNIPPET_TYPES } from '../types/Snippet' +import { createContextHook } from '../utils/bootstrap' +import { parseSnippetObject } from '../utils/snippets/objects' +import { getSnippetType } from '../utils/snippets/snippets' +import { useSnippetsList } from './useSnippetsList' +import type { Snippet, SnippetStatus, SnippetType } from '../types/Snippet' +import type { PropsWithChildren } from 'react' + +const fetchQueryParam = (name: string, validValues?: readonly T[]): T | undefined => { + const urlParams = new URLSearchParams(window.location.search) + const value = urlParams.get(name) + + if (!value) { + return undefined + } + + if (validValues) { + return validValues.includes(value as T) ? (value as T) : undefined + } + + return value as T +} + +const parseSearchQuery = (query?: string): [string | undefined, number | undefined] => { + const lineMatch = query?.trim().match(/@line:(?\d+)/) + const lineNumber = lineMatch?.groups?.line ? parseInt(lineMatch.groups.line, 10) : undefined + + return lineMatch && lineNumber + ? [query?.replace(lineMatch[0], '').trim(), lineNumber] + : [query, undefined] +} + +const updateQueryParam = (name: string, value?: string) => { + if ('URLSearchParams' in window) { + const searchParams = new URLSearchParams(window.location.search) + + if (value) { + searchParams.set(name, value) + } else { + searchParams.delete(name) + } + + const newUrl = window.location.toString().replace(window.location.search, `?${searchParams.toString()}`) + console.log(window.location.search, searchParams.toString(), newUrl) + window.history.replaceState({}, document.title, newUrl) + } +} + +const useFilterSnippetsList = ({ + currentTag, + currentType, + searchQueryText, + searchLineNumber +}: Pick): Snippet[] => { + const { snippetsList } = useSnippetsList() + const sanitizedSearchQueryText = searchQueryText?.toLowerCase().trim() + + const snippets = snippetsList ?? window.CODE_SNIPPETS_MANAGE?.snippetsList.map(parseSnippetObject) + + return useMemo( + () => snippets?.filter(snippet => { + if (currentType && getSnippetType(snippet) !== currentType) { + return false + } + + if (currentTag && !snippet.tags.includes(currentTag)) { + return false + } + + if (sanitizedSearchQueryText) { + if (searchLineNumber !== undefined) { + const codeLines = snippet.code.split('\n') + return codeLines[searchLineNumber]?.includes(sanitizedSearchQueryText) + } else { + const fields = ['name', 'desc', 'code', 'tags'] as const + + return fields.some(field => + ('tags' === field ? snippet.tags.join(' ') : snippet[field]) + .toLowerCase().includes(sanitizedSearchQueryText.toLowerCase())) + } + } + + return true + }) ?? [], + [snippets, currentTag, currentType, sanitizedSearchQueryText, searchLineNumber]) +} + +const useSnippetsByStatus = (snippets: Snippet[]) => + useMemo(() => + snippets.reduce((acc, snippet) => { + if (!acc.get(undefined)?.push(snippet)) { + acc.set(undefined, [snippet]) + } + + const status = snippet.lastActive + ? 'recently_activated' + : snippet.active ? 'active' : 'inactive' + + if (!acc.get(status)?.push(snippet)) { + acc.set(status, [snippet]) + } + + return acc + }, new Map()), + [snippets]) + +export interface SnippetsTableContext { + currentTag: string | undefined + currentType: SnippetType | undefined + searchQuery: string | undefined + currentStatus: SnippetStatus | undefined + setCurrentTag: (tag?: string) => void + setCurrentType: (type?: SnippetType) => void + setCurrentStatus: (status?: SnippetStatus) => void + setSearchQuery: (query?: string) => void + snippetsByStatus: Map + searchLineNumber?: number + searchQueryText?: string +} + +export const [SnippetsTableContext, useSnippetsTable] = createContextHook('useSnippetsTable') + +export const WithSnippetsTableContext: React.FC = ({ children }) => { + const [currentTag, setTag] = useState(() => fetchQueryParam('tag')) + const [currentType, setType] = useState(() => fetchQueryParam('type', SNIPPET_TYPES)) + const [currentStatus, setStatus] = useState(() => fetchQueryParam('status', SNIPPET_STATUSES)) + const [searchQuery, setSearch] = useState(() => fetchQueryParam('s')) + + const setCurrentType = useCallback((type?: SnippetType) => { + setType(type) + updateQueryParam('type', type) + }, [setType]) + + const setCurrentStatus = useCallback((status?: SnippetStatus) => { + setStatus(status) + updateQueryParam('status', status) + }, [setStatus]) + + const setCurrentTag = useCallback((tag?: string) => { + setTag(tag) + updateQueryParam('tag', tag) + }, [setTag]) + + const setSearchQuery = useCallback((query?: string) => { + setSearch(query) + updateQueryParam('s', query) + }, [setSearch]) + + const [searchQueryText, searchLineNumber] = useMemo(() => parseSearchQuery(searchQuery), [searchQuery]) + + const visibleSnippets = useFilterSnippetsList({ currentTag, currentType, searchLineNumber, searchQueryText }) + const snippetsByStatus = useSnippetsByStatus(visibleSnippets) + + const value: SnippetsTableContext = { + currentTag, + currentType, + searchQuery, + currentStatus, + setCurrentTag, + setCurrentType, + setSearchQuery, + setCurrentStatus, + snippetsByStatus, + searchLineNumber, + searchQueryText + } + + return {children} +} diff --git a/src/js/types/Snippet.ts b/src/js/types/Snippet.ts index 948826ef..d588d85b 100644 --- a/src/js/types/Snippet.ts +++ b/src/js/types/Snippet.ts @@ -11,11 +11,12 @@ export interface Snippet { readonly shared_network?: boolean | null readonly modified?: string readonly conditionId: number + readonly lastActive?: number readonly code_error?: readonly [string, number] | null } -export const SNIPPET_TYPES = ['php', 'html', 'css', 'js', 'cond'] as const -export const SNIPPET_STATUSES = ['active', 'inactive', 'recently_activated'] as const +export const SNIPPET_TYPES = ['php', 'html', 'css', 'js', 'cond'] +export const SNIPPET_STATUSES = ['active', 'inactive', 'recently_activated'] export type SnippetType = typeof SNIPPET_TYPES[number] export type SnippetStatus = typeof SNIPPET_STATUSES[number] diff --git a/src/js/types/Window.ts b/src/js/types/Window.ts index 027f0505..4c29c8b3 100644 --- a/src/js/types/Window.ts +++ b/src/js/types/Window.ts @@ -36,6 +36,10 @@ declare global { connectCloud: string } } + readonly CODE_SNIPPETS_MANAGE?: { + hasNetworkCap: boolean + snippetsList: Snippet[] + } readonly CODE_SNIPPETS_EDIT?: { snippet: Snippet pageTitleActions: Record diff --git a/src/js/types/schema/SnippetSchema.ts b/src/js/types/schema/SnippetSchema.ts index a765bf8e..41ac2a83 100644 --- a/src/js/types/schema/SnippetSchema.ts +++ b/src/js/types/schema/SnippetSchema.ts @@ -16,5 +16,6 @@ export interface WritableSnippetSchema { export interface SnippetSchema extends Readonly> { readonly id: number readonly modified: string + readonly last_active?: number readonly code_error?: readonly [string, number] | null } diff --git a/src/js/utils/bootstrap.tsx b/src/js/utils/bootstrap.tsx index 978a7429..671c2b99 100644 --- a/src/js/utils/bootstrap.tsx +++ b/src/js/utils/bootstrap.tsx @@ -1,20 +1,21 @@ -import { createContext, useContext } from 'react' +import React, { createContext, useContext } from 'react' import { createRoot } from 'react-dom/client' import type { Context, FunctionComponent } from 'react' -import React from 'react' export const loadComponent = (containerId: string, Component: FunctionComponent): void => { - const container = document.getElementById('snippets-table-container') + window.addEventListener('DOMContentLoaded', () => { + const container = document.getElementById(containerId) - if (container) { - const root = createRoot(container) - root.render() - } else { - console.error(`Could not find ${containerId.replace(/-_/, ' ')}.`) - } + if (container) { + const root = createRoot(container) + root.render() + } else { + console.error(`Could not find element #${containerId}.`) + } + }) } -export const createContextHook = (name: string): [ +export const createContextHook = (hookName: string): [ Context, () => T ] => { @@ -24,7 +25,7 @@ export const createContextHook = (name: string): [ const value = useContext(contextValue) if (value === undefined) { - throw Error(`use${name} can only be used within a ${name} context provider.`) + throw Error(`${hookName} can only be used within a corresponding context provider.`) } return value diff --git a/src/js/utils/files.ts b/src/js/utils/files.ts index f0a54880..119e7e2b 100644 --- a/src/js/utils/files.ts +++ b/src/js/utils/files.ts @@ -1,8 +1,10 @@ import { getSnippetType } from './snippets/snippets' +import type { SnippetsExport } from '../types/SnippetsExport' import type { Snippet } from '../types/Snippet' const SECOND_IN_MS = 1000 const TIMEOUT_SECONDS = 40 +const JSON_INDENT_SPACES = 2 const MIME_INFO = { php: ['php', 'text/php'], @@ -23,16 +25,19 @@ export const downloadAsFile = (content: BlobPart, filename: string, type: string } export const downloadSnippetExportFile = ( - content: BlobPart, + content: SnippetsExport | string, { id, name, scope }: Snippet, type?: keyof typeof MIME_INFO ) => { - const [ext, mimeType] = MIME_INFO[type ?? getSnippetType({ scope })] - const sanitizedName = name.toLowerCase().replace(/[^\w-]+/g, '-').trim() - const title = '' === sanitizedName ? `snippet-${id}` : sanitizedName - const filename = `${title}.code-snippets.${ext}` - downloadAsFile(content, filename, mimeType) + if ('string' === typeof content) { + const [ext, mimeType] = MIME_INFO[type ?? getSnippetType({ scope })] + const filename = `${title}.code-snippets.${ext}` + downloadAsFile(content, filename, mimeType) + } else { + const filename = `${title}.code-snippets.json` + downloadAsFile(JSON.stringify(content, undefined, JSON_INDENT_SPACES), filename, 'application/json') + } } diff --git a/src/js/utils/restAPI.ts b/src/js/utils/restAPI.ts index 1a6b14e8..f2888bc6 100644 --- a/src/js/utils/restAPI.ts +++ b/src/js/utils/restAPI.ts @@ -1,6 +1,8 @@ import { trimTrailingChar } from './text' import type { AxiosRequestConfig } from 'axios' +export const REST_API_NAMESPACE = 'code-snippets' + export const REST_BASE = trimTrailingChar(window.CODE_SNIPPETS?.restAPI.base ?? '', '/') export const REST_SNIPPETS_BASE = trimTrailingChar(window.CODE_SNIPPETS?.restAPI.snippets ?? '', '/') diff --git a/src/js/utils/snippets/objects.ts b/src/js/utils/snippets/objects.ts index 09c9c272..ee74c2a2 100644 --- a/src/js/utils/snippets/objects.ts +++ b/src/js/utils/snippets/objects.ts @@ -1,5 +1,20 @@ -import type { Snippet, SnippetScope } from '../../types/Snippet' import { SNIPPET_TYPE_SCOPES } from '../../types/Snippet' +import { isNetworkAdmin } from '../screen' +import type { Snippet, SnippetScope } from '../../types/Snippet' + +const defaults: Omit = { + id: 0, + name: '', + code: '', + desc: '', + scope: 'global', + modified: '', + active: false, + network: isNetworkAdmin(), + shared_network: null, + priority: 10, + conditionId: 0 +} const isAbsInt = (value: unknown): value is number => 'number' === typeof value && 0 < value @@ -11,22 +26,28 @@ export const isValidScope = (scope: unknown): scope is SnippetScope => 'string' === typeof scope && Object.values(SNIPPET_TYPE_SCOPES).some(typeScopes => typeScopes.some(typeScope => typeScope === scope)) -export const parseSnippetObject = (fields: unknown, defaults: Snippet): Snippet => - 'object' !== typeof fields || null === fields - ? defaults - : { - id: 'id' in fields && isAbsInt(fields.id) ? fields.id : defaults.id, - name: 'name' in fields && 'string' === typeof fields.name ? fields.name : defaults.name, - desc: 'desc' in fields && 'string' === typeof fields.desc ? fields.desc : defaults.desc, - code: 'code' in fields && 'string' === typeof fields.code ? fields.code : defaults.code, - tags: 'tags' in fields ? parseStringArray(fields.tags) ?? defaults.tags : defaults.tags, - scope: 'scope' in fields && isValidScope(fields.scope) ? fields.scope : defaults.scope, - modified: 'modified' in fields && 'string' === typeof fields.modified ? fields.modified : defaults.modified, - active: 'active' in fields && 'boolean' === typeof fields.active ? fields.active : defaults.active, - network: 'network' in fields && 'boolean' === typeof fields.network ? fields.network : defaults.network, - shared_network: 'shared_network' in fields && 'boolean' === typeof fields.shared_network && fields.shared_network || - defaults.shared_network, - priority: 'priority' in fields && 'number' === typeof fields.priority ? fields.priority : defaults.priority, - conditionId: 'condition_id' in fields && isAbsInt(fields.condition_id) ? fields.condition_id - : 'conditionId' in fields && isAbsInt(fields.conditionId) ? fields.conditionId : defaults.conditionId - } +export const parseSnippetObject = (fields: unknown): Snippet => { + const result: { -readonly [F in keyof Snippet]: Snippet[F] } = { ...defaults, tags: [] } + + if ('object' !== typeof fields || null === fields) { + return result + } + + return { + id: 'id' in fields && isAbsInt(fields.id) ? fields.id : defaults.id, + name: 'name' in fields && 'string' === typeof fields.name ? fields.name : defaults.name, + desc: 'desc' in fields && 'string' === typeof fields.desc ? fields.desc : defaults.desc, + code: 'code' in fields && 'string' === typeof fields.code ? fields.code : defaults.code, + tags: 'tags' in fields ? parseStringArray(fields.tags) ?? [] : [], + scope: 'scope' in fields && isValidScope(fields.scope) ? fields.scope : defaults.scope, + modified: 'modified' in fields && 'string' === typeof fields.modified ? fields.modified : defaults.modified, + active: 'active' in fields && 'boolean' === typeof fields.active ? fields.active : defaults.active, + network: 'network' in fields && 'boolean' === typeof fields.network ? fields.network : defaults.network, + shared_network: 'shared_network' in fields && 'boolean' === typeof fields.shared_network && fields.shared_network || + defaults.shared_network, + priority: 'priority' in fields && 'number' === typeof fields.priority ? fields.priority : defaults.priority, + conditionId: 'condition_id' in fields && isAbsInt(fields.condition_id) ? fields.condition_id + : 'conditionId' in fields && isAbsInt(fields.conditionId) ? fields.conditionId : defaults.conditionId, + lastActive: 'last_active' in fields ? Number(fields.last_active) : undefined + } +} diff --git a/src/js/utils/snippets/snippets.ts b/src/js/utils/snippets/snippets.ts index 4a3a5d20..73529206 100644 --- a/src/js/utils/snippets/snippets.ts +++ b/src/js/utils/snippets/snippets.ts @@ -1,5 +1,5 @@ import { __ } from '@wordpress/i18n' -import { isNetworkAdmin } from '../screen' +import { addQueryArgs } from '@wordpress/url' import { parseSnippetObject } from './objects' import type { Snippet, SnippetType } from '../../types/Snippet' @@ -13,22 +13,8 @@ export const SNIPPET_TYPE_LABELS: Record = { const PRO_TYPES = new Set(['css', 'js', 'cond']) -const defaults: Omit = { - id: 0, - name: '', - code: '', - desc: '', - scope: 'global', - modified: '', - active: false, - network: isNetworkAdmin(), - shared_network: null, - priority: 10, - conditionId: 0 -} - export const createSnippetObject = (fields: unknown = null): Snippet => - parseSnippetObject(fields, { ...defaults, tags: [] }) + parseSnippetObject(fields) export const getSnippetType = ({ scope }: Pick): SnippetType => { switch (true) { @@ -49,6 +35,11 @@ export const getSnippetType = ({ scope }: Pick): SnippetType = } } +export const getSnippetEditUrl = (snippet?: Pick): string | undefined => + snippet?.id + ? addQueryArgs(window.CODE_SNIPPETS?.urls.edit, { id: snippet.id }) + : window.CODE_SNIPPETS?.urls.addNew + export const validateSnippet = (snippet: Snippet): undefined | string => { const missingTitle = '' === snippet.name.trim() const missingCode = '' === snippet.code.trim() diff --git a/src/php/admin-menus/class-edit-menu.php b/src/php/admin-menus/class-edit-menu.php index 75e77c86..e8a99290 100644 --- a/src/php/admin-menus/class-edit-menu.php +++ b/src/php/admin-menus/class-edit-menu.php @@ -67,7 +67,7 @@ public function register() { $this->add_menu( code_snippets()->get_menu_slug( 'add' ), _x( 'Add New', 'menu label', 'code-snippets' ), - __( 'Create New Snippet', 'code-snippets' ) + __( 'Add New Snippet', 'code-snippets' ) ); } diff --git a/src/php/admin-menus/class-manage-menu.php b/src/php/admin-menus/class-manage-menu.php index 7b711820..af8a68cb 100644 --- a/src/php/admin-menus/class-manage-menu.php +++ b/src/php/admin-menus/class-manage-menu.php @@ -3,6 +3,7 @@ namespace Code_Snippets; use Code_Snippets\Cloud\Cloud_Search_List_Table; +use Code_Snippets\REST_API\Snippets_REST_Controller; use function Code_Snippets\Settings\get_setting; /** @@ -221,7 +222,13 @@ public function enqueue_assets() { self::JS_HANDLE, 'CODE_SNIPPETS_MANAGE', [ - 'pageTitleActions' => $plugin->is_compact_menu() ? $this->page_title_action_links( [ 'add', 'import', 'settings' ] ) : [], + 'hasNetworkCap' => current_user_can( code_snippets()->get_network_cap_name() ), + 'snippetsList' => array_map( + function ( $snippet ) { + return $snippet->get_fields(); + }, + get_snippets() + ), ] ); } diff --git a/src/php/class-plugin.php b/src/php/class-plugin.php index 973281d0..14950c8c 100644 --- a/src/php/class-plugin.php +++ b/src/php/class-plugin.php @@ -89,7 +89,6 @@ public function __construct( string $version, string $file ) { add_filter( 'admin_url', array( $this, 'add_safe_mode_query_var' ) ); } - add_action( 'rest_api_init', [ $this, 'init_rest_api' ] ); add_action( 'allowed_redirect_hosts', [ $this, 'allow_code_snippets_redirect' ] ); } @@ -124,22 +123,13 @@ public function load_plugin() { $this->active_snippets = new Active_Snippets(); $this->front_end = new Front_End(); $this->cloud_api = new Cloud_API(); + new Rest_API(); $upgrade = new Upgrade( $this->version, $this->db ); add_action( 'plugins_loaded', array( $upgrade, 'run' ), 0 ); $this->licensing = new Licensing(); } - /** - * Register custom REST API controllers. - * - * @return void - */ - public function init_rest_api() { - $snippets_controller = new Snippets_REST_Controller(); - $snippets_controller->register_routes(); - } - /** * Disable snippet execution if the necessary query var is set. * diff --git a/src/php/class-snippet.php b/src/php/class-snippet.php index ecab62c9..992df301 100644 --- a/src/php/class-snippet.php +++ b/src/php/class-snippet.php @@ -5,6 +5,7 @@ use DateTime; use DateTimeZone; use Exception; +use function Code_Snippets\Settings\get_self_option; /** * A snippet object. @@ -28,6 +29,7 @@ * @property int $revision Revision or version number of snippet. * @property string $cloud_id Cloud ID and ownership status of snippet. * + * @property-read int $last_active Timestamp of when the snippet was last active, if available. * @property-read string $display_name The snippet name if it exists or a placeholder if it does not. * @property-read string $tags_list The tags in string list format. * @property-read string $scope_icon The dashicon used to represent the current scope. @@ -202,6 +204,16 @@ public static function get_types(): array { return [ 'php', 'html', 'css', 'js', 'cond' ]; } + /** + * Retrieve the timestamp of when the snippet was last active, if available. + * + * @return string + */ + protected function get_last_active(): string { + $recently_active = get_self_option( $this->network, 'recently_activated_snippets', [] ); + return $recently_active[ (string) $this->id ] ?? 0; + } + /** * Determine the language that the snippet code is written in, based on the scope * diff --git a/src/php/rest-api/class-rest-api.php b/src/php/rest-api/class-rest-api.php new file mode 100644 index 00000000..088697b3 --- /dev/null +++ b/src/php/rest-api/class-rest-api.php @@ -0,0 +1,129 @@ +register_routes(); + $this->register_routes(); + } + + /** + * Register REST routes. + */ + public function register_routes() { + register_rest_route( + $this->namespace, + 'recently-active', + [ + [ + 'methods' => WP_REST_Server::READABLE, + 'callback' => [ $this, 'get_recent_list_callback' ], + 'permission_callback' => [ code_snippets(), 'current_user_can' ], + 'args' => [ + 'network' => [ + 'description' => esc_html__( 'Fetch the recent list for network-wide snippets instead of site-wide.', 'code-snippets' ), + 'type' => 'boolean', + 'default' => false, + ], + ], + ], + ] + ); + + register_rest_route( + $this->namespace, + 'recently-active', + [ + [ + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => [ $this, 'clear_recent_list_callback' ], + 'permission_callback' => [ code_snippets(), 'current_user_can' ], + 'args' => [ + 'network' => [ + 'description' => esc_html__( 'Clear the recent list for network-wide snippets instead of site-wide.', 'code-snippets' ), + 'type' => 'boolean', + 'default' => false, + ], + ], + ], + ] + ); + } + + /** + * Callback for retrieving the recently activated snippets list. + * + * This will return the list of recently activated snippets, either site-wide or network-wide, + * depending on the 'network' parameter. + * + * @param WP_REST_Request $request The REST request object. + * + * @return WP_REST_Response The recently activated snippets list. + */ + public function get_recent_list_callback( WP_REST_Request $request ): WP_REST_Response { + return rest_ensure_response( + $request->get_param( 'network' ) + ? get_site_option( 'recently_activated_snippets', [] ) + : get_option( 'recently_activated_snippets', [] ) + ); + } + + /** + * Callback for clearing the recently activated snippets list. + * + * This will clear the list of recently activated snippets, either site-wide or network-wide, + * depending on the 'network' parameter. + * + * @param WP_REST_Request $request The REST request object. + * + * @return WP_REST_Response The recently activated snippets list prior to clearing it. + */ + public function clear_recent_list_callback( WP_REST_Request $request ): WP_REST_Response { + if ( $request->get_param( 'network' ) ) { + $current = get_site_option( 'recently_activated_snippets', [] ); + update_site_option( 'recently_activated_snippets', [] ); + } else { + $current = get_option( 'recently_activated_snippets', [] ); + update_option( 'recently_activated_snippets', [] ); + } + + return rest_ensure_response( $current ); + } +} diff --git a/src/php/rest-api/class-snippets-rest-controller.php b/src/php/rest-api/class-snippets-rest-controller.php index 036e5470..77fe9c0a 100644 --- a/src/php/rest-api/class-snippets-rest-controller.php +++ b/src/php/rest-api/class-snippets-rest-controller.php @@ -547,6 +547,11 @@ public function get_item_schema(): array { 'format' => 'date-time', 'readonly' => true, ], + 'last_active' => [ + 'description' => esc_html__( 'Timestamp of when the snippet was last active, if available.', 'code-snippets' ), + 'type' => 'integer', + 'readonly' => true, + ], 'code_error' => [ 'description' => esc_html__( 'Error message if the snippet code could not be parsed.', 'code-snippets' ), 'type' => 'string', diff --git a/src/php/snippet-ops.php b/src/php/snippet-ops.php index 65420aa8..aa77d992 100644 --- a/src/php/snippet-ops.php +++ b/src/php/snippet-ops.php @@ -445,6 +445,13 @@ function delete_snippet( int $id, ?bool $network = null ): bool { do_action( 'code_snippets/delete_snippet', $id, $network ); clean_snippets_cache( $table ); code_snippets()->cloud_api->delete_snippet_from_transient_data( $id ); + + $recently_active = get_self_option( $network, 'recently_activated_snippets', [] ); + + if ( isset( $recently_active[ $id ] ) ) { + unset( $recently_active[ $id ] ); + update_self_option( $network, 'recently_activated_snippets', $recently_active ); + } } return (bool) $result; @@ -492,7 +499,7 @@ function test_snippet_code( Snippet $snippet ) { * * @since 2.0.0 */ -function save_snippet( $snippet ) { +function save_snippet( $snippet ): ?Snippet { global $wpdb; $table = code_snippets()->db->get_table_name( $snippet->network ); @@ -551,19 +558,34 @@ function save_snippet( $snippet ) { $snippet->id = $wpdb->insert_id; do_action( 'code_snippets/create_snippet', $snippet, $table ); } else { + $existing = get_snippet( $snippet->id, $snippet->network ); // Otherwise, update the snippet data. $result = $wpdb->update( $table, $data, [ 'id' => $snippet->id ], null, [ '%d' ] ); + if ( false === $result ) { return null; } - do_action( 'code_snippets/update_snippet', $snippet, $table ); + $updated = get_snippet( $snippet->id, $snippet->network ); + do_action( 'code_snippets/update_snippet', $updated, $table, $existing ); + + if ( ! $updated->active && $existing->active ) { + $recently_active = [ $updated->id => time() ] + get_self_option( $updated->network, 'recently_activated_snippets', [] ); + update_self_option( $updated->network, 'recently_activated_snippets', $recently_active ); + } elseif ( ! $updated->active ) { + $recently_active = get_self_option( $updated->network, 'recently_activated_snippets', [] ); + + if ( isset( $recently_active[ $updated->id ] ) ) { + unset( $recently_active[ $updated->id ] ); + update_self_option( $updated->network, 'recently_activated_snippets', $recently_active ); + } + } } - update_shared_network_snippets( [ $snippet ] ); + update_shared_network_snippets( [ $updated ] ); clean_snippets_cache( $table ); - return $snippet; + return $updated; } /** From 00935b222e837de6b3ea293121c34426fcfd51ba Mon Sep 17 00:00:00 2001 From: Shea Bunge Date: Fri, 25 Jul 2025 01:02:29 +1000 Subject: [PATCH 048/318] Add upsell dialog to manage page. --- src/css/manage.scss | 1 + .../SnippetsTable/SnippetsTable.tsx | 71 ++++++++++++------- 2 files changed, 45 insertions(+), 27 deletions(-) diff --git a/src/css/manage.scss b/src/css/manage.scss index e55346f9..1582d8eb 100644 --- a/src/css/manage.scss +++ b/src/css/manage.scss @@ -1,6 +1,7 @@ @use 'common/badges'; @use 'common/switch'; @use 'common/select'; +@use 'common/upsell'; .nav-tab { display: flex; diff --git a/src/js/components/SnippetsTable/SnippetsTable.tsx b/src/js/components/SnippetsTable/SnippetsTable.tsx index bc938e05..8a6a84fa 100644 --- a/src/js/components/SnippetsTable/SnippetsTable.tsx +++ b/src/js/components/SnippetsTable/SnippetsTable.tsx @@ -1,41 +1,50 @@ import { __, sprintf } from '@wordpress/i18n' -import React from 'react' +import React, { useState } from 'react' import classnames from 'classnames' import { addQueryArgs } from '@wordpress/url' import { WithRestAPIContext } from '../../hooks/useRestAPI' import { WithSnippetsListContext } from '../../hooks/useSnippetsList' import { WithSnippetsTableContext, useSnippetsTable } from '../../hooks/useSnippetsTable' import { SNIPPET_TYPES } from '../../types/Snippet' -import { SNIPPET_TYPE_LABELS } from '../../utils/snippets/snippets' +import { isLicensed } from '../../utils/screen' +import { isProType, SNIPPET_TYPE_LABELS } from '../../utils/snippets/snippets' import { Badge } from '../common/Badge' import { Button } from '../common/Button' +import { UpsellDialog } from '../common/UpsellDialog' import { SnippetsListTable } from './SnippetsListTable' import type { SnippetType } from '../../types/Snippet' -import type { MouseEventHandler } from 'react' interface SnippetTypeTabProps { type?: SnippetType + setIsUpgradeDialogOpen: (isOpen: boolean) => void } -const SnippetTypeTab: React.FC = ({ type }) => { +const SnippetTypeTab: React.FC = ({ type, setIsUpgradeDialogOpen }) => { const { currentType, setCurrentType } = useSnippetsTable() const tabName = type ?? 'all' - const handleClick: MouseEventHandler = event => { - event.preventDefault() - setCurrentType(type) - } - return ( { + event.preventDefault() + + if (type && !isLicensed() && isProType(type)) { + setIsUpgradeDialogOpen(true) + } else { + setCurrentType(type) + } + }} > - - {type ? SNIPPET_TYPE_LABELS[type] : __('All Snippets', 'code-snippets')} - - {type && } + + {type ? SNIPPET_TYPE_LABELS[type] : __('All Snippets', 'code-snippets')} + + {type && + } ) } @@ -51,13 +60,13 @@ const PageHeading = () => { {__('Search results', 'code-snippets')} {/* translators: %s: search query. */} - {searchQueryText && sprintf( __( ' for “%s”', 'code-snippets' ), searchQueryText )} + {searchQueryText && sprintf(__(' for “%s”', 'code-snippets'), searchQueryText)} {/* translators: %s: search query. */} - {searchLineNumber && sprintf( __( ' on line “%d”', 'code-snippets' ), searchLineNumber )} + {searchLineNumber && sprintf(__(' on line “%d”', 'code-snippets'), searchLineNumber)} {/* translators: %s: tag name. */} - {currentTag && sprintf( __( ' in tag “%s”', 'code-snippets' ), currentTag )} + {currentTag && sprintf(__(' in tag “%s”', 'code-snippets'), currentTag)} {' '} - - {confirmDeleteDialogOpen - ? setConfirmDeleteDialogOpen(false)} - closeButtonLabel={__('Cancel', 'code-snippets')} - > - {__('You are about to permanently delete this snippet.', 'code-snippets')} - - - - - - : null} - - ) + default: { + const actionText = snippet.network && !snippet.shared_network + ? snippet.active ? __('Network Deactivate', 'code-snippets') : __('Network Activate', 'code-snippets') + : snippet.active ? __('Deactivate', 'code-snippets') : __('Activate', 'code-snippets') + + return ( + <> + + + { + (snippet.active ? deactivate(snippet) : activate(snippet)) + .then(() => refreshSnippetsList()) + .catch(handleUnknownError) + }} + /> + + ) + } + } } const RowActions: React.FC = ({ snippet }) => { const { snippetsAPI } = useRestAPI() + const { refreshSnippetsList } = useSnippetsList() if (!isNetworkAdmin() && snippet.network && !snippet.shared_network) { return ( @@ -100,6 +96,20 @@ const RowActions: React.FC = ({ snippet }) => {
{__('Edit', 'code-snippets')}{' | '} + {' | '} + {' | '} - +
) } -const NameColumn: React.FC = ({ snippet }) => { - // translators: %s: snippet identifier. - const displayName = snippet.name.trim() ? snippet.name : sprintf(__('Snippet #%d', 'code-snippets'), snippet.id) +const NameColumn: React.FC = ({ snippet }) => + <> + {isNetworkAdmin() || !snippet.network || window.CODE_SNIPPETS_MANAGE?.hasNetworkCap + ? {getSnippetDisplayName(snippet)} + : getSnippetDisplayName(snippet)} - return ( - <> - {isNetworkAdmin() || !snippet.network || window.CODE_SNIPPETS_MANAGE?.hasNetworkCap - ? {displayName} - : displayName} + {snippet.shared_network && {__('Shared on Network', 'code-snippets')}} + + + - {snippet.shared_network && {__('Shared on Network', 'code-snippets')}} +const TypeColumn: React.FC = ({ snippet }) => { + const { setCurrentType } = useSnippetsFilters() + const type = getSnippetType(snippet) - - + return ( + { + event.preventDefault() + setCurrentType(type) + }} + > + + ) } +const TagsColumn: React.FC = ({ snippet }) => + snippet.tags.map((tag, index) => + + + {tag} + + {index < snippet.tags.length - 1 ? ', ' : ''} + ) + +const DateColumn: React.FC = ({ snippet }) => + snippet.modified + ? + + + : <>— + const PriorityColumn: React.FC = ({ snippet }) => { const [value, setValue] = useState(snippet.priority) const { snippetsAPI } = useRestAPI() @@ -175,41 +214,30 @@ export const TableColumns: ListTableColumn[] = [ id: 'name', title: __('Name', 'code-snippets'), isPrimary: true, - sortedValue: item => item.name.toLowerCase(), + sortedValue: snippet => getSnippetDisplayName(snippet).toLowerCase(), render: snippet => }, { id: 'type', title: __('Type', 'code-snippets'), - sortedValue: item => getSnippetType(item), - render: snippet => + sortedValue: snippet => getSnippetType(snippet), + render: snippet => }, { id: 'desc', title: __('Description', 'code-snippets'), - // TODO: figure out how to allow formatting and markup. - render: snippet => stripTags(snippet.desc) + render: snippet => {snippet.desc} }, { id: 'tags', title: __('Tags', 'code-snippets'), - render: snippet => - snippet.tags.map((tag, index) => - - - {tag} - - {index < snippet.tags.length - 1 ? ', ' : ''} - - ) + render: snippet => }, { id: 'date', title: __('Modified', 'code-snippets'), sortedValue: snippet => snippet.modified ? new Date(snippet.modified).toISOString() : '', - render: snippet => snippet.modified - ? - : '—' + render: snippet => }, { id: 'priority', diff --git a/src/js/components/EditorSidebar/actions/DeleteButton.tsx b/src/js/components/common/DeleteButton.tsx similarity index 51% rename from src/js/components/EditorSidebar/actions/DeleteButton.tsx rename to src/js/components/common/DeleteButton.tsx index 1dc7f1cb..97eaed04 100644 --- a/src/js/components/EditorSidebar/actions/DeleteButton.tsx +++ b/src/js/components/common/DeleteButton.tsx @@ -1,22 +1,34 @@ -import { addQueryArgs } from '@wordpress/url' import React, { useState } from 'react' import { __ } from '@wordpress/i18n' -import { useRestAPI } from '../../../hooks/useRestAPI' -import { Button } from '../../common/Button' -import { ConfirmDialog } from '../../common/ConfirmDialog' -import { useSnippetForm } from '../../../hooks/useSnippetForm' +import { useRestAPI } from '../../hooks/useRestAPI' +import { Button } from './Button' +import { ConfirmDialog } from './ConfirmDialog' +import type { Snippet } from '../../types/Snippet' +import type { ButtonProps } from './Button' -export const DeleteButton: React.FC = () => { +export interface DeleteButtonProps extends ButtonProps { + snippet: Snippet + setIsWorking?: (isWorking: boolean) => void + onSuccess?: () => Promise | void + onError?: (error: unknown) => void +} + +export const DeleteButton: React.FC = ({ + snippet, + onSuccess, + onError, + className = 'delete-button', + setIsWorking, + ...buttonProps +}) => { const { snippetsAPI } = useRestAPI() - const { snippet, setIsWorking, isWorking, handleRequestError } = useSnippetForm() const [isDialogOpen, setIsDialogOpen] = useState(false) return ( <>
+ : null +} diff --git a/src/js/components/WelcomePage/Changelog.tsx b/src/js/components/WelcomePage/Changelog.tsx index dd5c4adb..30366732 100644 --- a/src/js/components/WelcomePage/Changelog.tsx +++ b/src/js/components/WelcomePage/Changelog.tsx @@ -1,7 +1,7 @@ import React, { Fragment } from 'react' +import { __, sprintf } from '@wordpress/i18n' import { CHANGELOG_SECTIONS } from '../../types/schema/WelcomeSchema' import type { ChangelogSectionTitle } from '../../types/schema/WelcomeSchema' -import { __, sprintf } from '@wordpress/i18n' const CHANGELOG_LABELS: Record = { Added: __('New features', 'code-snippets'), @@ -61,13 +61,13 @@ export const Changelog = () => {__('View changelog', 'code-snippets')}
- {CHANGELOG_DATA && CHANGELOG_DATA.map(({ version, date, entries }) => + {CHANGELOG_DATA?.map(({ version, date, entries }) =>
{/* translators: %s: version number. */} diff --git a/src/js/components/WelcomePage/WelcomePage.tsx b/src/js/components/WelcomePage/WelcomePage.tsx index 8a1edea3..6ef33fba 100644 --- a/src/js/components/WelcomePage/WelcomePage.tsx +++ b/src/js/components/WelcomePage/WelcomePage.tsx @@ -1,8 +1,8 @@ -import { __ } from "@wordpress/i18n" -import React, { useState } from "react" -import { ImageLinkSchema } from '../../types/schema/WelcomeSchema' +import { __ } from '@wordpress/i18n' +import React, { useState } from 'react' import { Toolbar } from '../common/Toolbar' import { Changelog } from './Changelog' +import type { ImageLinkSchema } from '../../types/schema/WelcomeSchema' const DATA = window.CODE_SNIPPETS_WELCOME @@ -58,7 +58,7 @@ const Partners: React.FC = ({ partners }) => interface ArticlesProps { - articles: ImageLinkSchema[] + articles: ImageLinkSchema[] } const Articles: React.FC = ({ articles }) => diff --git a/src/js/components/common/Toolbar.tsx b/src/js/components/common/Toolbar.tsx index b390f493..23039d97 100644 --- a/src/js/components/common/Toolbar.tsx +++ b/src/js/components/common/Toolbar.tsx @@ -1,10 +1,11 @@ import { __ } from '@wordpress/i18n' import classnames from 'classnames' -import React, { ReactNode, useState } from 'react' +import React, { useState } from 'react' import { isLicensed, shouldShowUpsell } from '../../utils/screen' import { fetchQueryParam } from '../../utils/urls' import { CommunityIcon, LibraryIcon, SettingsIcon, SnippetsIcon, TeamsIcon } from './icons/ToolbarIcons' import { UpsellDialog } from './UpsellDialog' +import type { ReactNode } from 'react' interface NavLink { name: string diff --git a/src/js/hooks/useSnippetsAPI.ts b/src/js/hooks/useSnippetsAPI.ts deleted file mode 100644 index 5805e35a..00000000 --- a/src/js/hooks/useSnippetsAPI.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { useEffect, useMemo, useState } from 'react' -import { addQueryArgs } from '@wordpress/url' -import { handleUnknownError } from '../utils/errors' -import { REST_API_AXIOS_CONFIG, REST_SNIPPETS_BASE } from '../utils/restAPI' -import { isNetworkAdmin } from '../utils/screen' -import { createSnippetObject } from '../utils/snippets/snippets' -import { useAxios } from './useAxios' -import type { Snippet } from '../types/Snippet' -import type { SnippetsExport } from '../types/SnippetsExport' - -export interface SnippetsAPI { - fetchAll: (network?: boolean | null) => Promise - fetch: (snippetId: number, network?: boolean | null) => Promise - create: (snippet: Snippet) => Promise - update: (snippet: Snippet) => Promise - delete: (snippet: Snippet) => Promise - activate: (snippet: Snippet) => Promise - deactivate: (snippet: Snippet) => Promise - export: (snippet: Snippet) => Promise - exportCode: (snippet: Snippet) => Promise -} - -const buildURL = ({ id, network }: Snippet, action?: string) => - addQueryArgs( - [REST_SNIPPETS_BASE, id, action].filter(Boolean).join('/'), - { network: network ? true : undefined } - ) - -export const useSnippetsAPI = (): SnippetsAPI => { - const { get, post, del } = useAxios(REST_API_AXIOS_CONFIG) - - return useMemo((): SnippetsAPI => ({ - fetchAll: network => - get(addQueryArgs(REST_SNIPPETS_BASE, { network })), - - fetch: (snippetId, network) => - get(addQueryArgs(`${REST_SNIPPETS_BASE}/${snippetId}`, { network })), - - create: snippet => - post(REST_SNIPPETS_BASE, snippet), - - update: snippet => - post(buildURL(snippet), snippet), - - delete: (snippet: Snippet) => - del(buildURL(snippet)), - - activate: snippet => - post(buildURL(snippet, 'activate')), - - deactivate: snippet => - post(buildURL(snippet, 'deactivate')), - - export: snippet => - get(buildURL(snippet, 'export')), - - exportCode: snippet => - get(buildURL(snippet, 'export-code')) - }), [get, post, del]) -} - -export const useSnippets = (): Snippet[] | undefined => { - const api = useSnippetsAPI() - const [snippets, setSnippets] = useState() - - useEffect(() => { - if (!snippets) { - api.fetchAll(isNetworkAdmin()) - .then(response => - setSnippets(response.map(snippet => createSnippetObject(snippet)))) - .catch(handleUnknownError) - } - }, [api, snippets]) - - return snippets -} diff --git a/src/js/types/KeyboardShortcut.ts b/src/js/types/KeyboardShortcut.ts index 58e647fb..f94a54c6 100644 --- a/src/js/types/KeyboardShortcut.ts +++ b/src/js/types/KeyboardShortcut.ts @@ -1,31 +1 @@ import { _x } from '@wordpress/i18n' - -export const KEYBOARD_KEYS = { - 'Cmd': _x('Cmd', 'keyboard key', 'code-snippets'), - 'Ctrl': _x('Ctrl', 'keyboard key', 'code-snippets'), - 'Shift': _x('Shift', 'keyboard key', 'code-snippets'), - 'Option': _x('Option', 'keyboard key', 'code-snippets'), - 'Alt': _x('Alt', 'keyboard key', 'code-snippets'), - 'Tab': _x('Tab', 'keyboard key', 'code-snippets'), - 'Up': _x('Up', 'keyboard key', 'code-snippets'), - 'Down': _x('Down', 'keyboard key', 'code-snippets'), - 'A': _x('A', 'keyboard key', 'code-snippets'), - 'D': _x('D', 'keyboard key', 'code-snippets'), - 'F': _x('F', 'keyboard key', 'code-snippets'), - 'G': _x('G', 'keyboard key', 'code-snippets'), - 'R': _x('R', 'keyboard key', 'code-snippets'), - 'S': _x('S', 'keyboard key', 'code-snippets'), - 'Y': _x('Y', 'keyboard key', 'code-snippets'), - 'Z': _x('Z', 'keyboard key', 'code-snippets'), - '/': _x('/', 'keyboard key', 'code-snippets'), - '[': _x(']', 'keyboard key', 'code-snippets'), - ']': _x(']', 'keyboard key', 'code-snippets') -} - -export type KeyboardKey = keyof typeof KEYBOARD_KEYS - -export interface KeyboardShortcut { - label: string - mod: KeyboardKey | KeyboardKey[] - key: KeyboardKey -} diff --git a/src/js/types/Window.ts b/src/js/types/Window.ts index 45adab8c..85ec3dce 100644 --- a/src/js/types/Window.ts +++ b/src/js/types/Window.ts @@ -1,6 +1,6 @@ +import type { ChangelogSchema, ImageLinkSchema } from './schema/WelcomeSchema' import type Prism from 'prismjs' import type tinymce from 'tinymce' -import { ChangelogSchema, ImageLinkSchema } from './schema/WelcomeSchema' import type { Snippet } from './Snippet' import type { CodeEditorInstance, EditorOption, WordPressCodeEditor } from './WordPressCodeEditor' import type { WordPressEditor } from './WordPressEditor' diff --git a/src/js/types/schema/WelcomeSchema.ts b/src/js/types/schema/WelcomeSchema.ts index ed5f9dfd..c9f5cd09 100644 --- a/src/js/types/schema/WelcomeSchema.ts +++ b/src/js/types/schema/WelcomeSchema.ts @@ -1,17 +1,12 @@ -export type ChangelogSchema = { +export interface ChangelogSchema { version: string date: string entries: ChangelogEntriesSchema } -export type ChangelogEntriesSchema = { - [section in ChangelogSectionTitle]?: { - core?: string[] - pro?: string[] - } -} +export type ChangelogEntriesSchema = Partial> -export const CHANGELOG_SECTIONS = ['Added', 'Changed', 'Fixed', 'Deprecated', 'Removed', 'Security', 'Other'] as const +export const CHANGELOG_SECTIONS = ['Added', 'Changed', 'Fixed', 'Deprecated', 'Removed', 'Security', 'Other'] export type ChangelogSectionTitle = typeof CHANGELOG_SECTIONS[number] export interface ImageLinkSchema { diff --git a/src/php/rest-api/class-rest-api.php b/src/php/rest-api/class-rest-api.php index 088697b3..db8e4acc 100644 --- a/src/php/rest-api/class-rest-api.php +++ b/src/php/rest-api/class-rest-api.php @@ -6,6 +6,8 @@ use WP_REST_Request; use WP_REST_Response; use WP_REST_Server; +use function Code_Snippets\Settings\delete_self_option; +use function Code_Snippets\Settings\get_self_option; /** * Class for managing the REST API functionality of the plugin. @@ -116,13 +118,10 @@ public function get_recent_list_callback( WP_REST_Request $request ): WP_REST_Re * @return WP_REST_Response The recently activated snippets list prior to clearing it. */ public function clear_recent_list_callback( WP_REST_Request $request ): WP_REST_Response { - if ( $request->get_param( 'network' ) ) { - $current = get_site_option( 'recently_activated_snippets', [] ); - update_site_option( 'recently_activated_snippets', [] ); - } else { - $current = get_option( 'recently_activated_snippets', [] ); - update_option( 'recently_activated_snippets', [] ); - } + $network = $request->get_param( 'network' ); + + $current = get_self_option( $network, 'recently_activated_snippets', [] ); + delete_self_option( $network, 'recently_activated_snippets' ); return rest_ensure_response( $current ); } diff --git a/src/php/settings/settings.php b/src/php/settings/settings.php index cf94f8b9..7f724edf 100644 --- a/src/php/settings/settings.php +++ b/src/php/settings/settings.php @@ -16,19 +16,6 @@ const OPTION_GROUP = 'code-snippets'; const OPTION_NAME = 'code_snippets_settings'; -/** - * Add a new option for either the current site or the current network - * - * @param bool $network Whether to add a network-wide option. - * @param string $option Name of option to add. Expected to not be SQL-escaped. - * @param mixed $value Option value, can be anything. Expected to not be SQL-escaped. - * - * @return bool False if the option was not added. True if the option was added. - */ -function add_self_option( bool $network, string $option, $value ): bool { - return $network ? add_site_option( $option, $value ) : add_option( $option, $value ); -} - /** * Retrieves an option value based on an option name from either the current site or the current network * @@ -43,7 +30,7 @@ function get_self_option( bool $network, string $option, $default_value = false } /** - * Update the value of an option that was already added on the current site or the current network + * Update the value of an option that was already added on the current site or the current network. * * @param bool $network Whether to update a network-wide option. * @param string $option Name of option. Expected to not be SQL-escaped. @@ -55,6 +42,18 @@ function update_self_option( bool $network, string $option, $value ): bool { return $network ? update_site_option( $option, $value ) : update_option( $option, $value ); } +/** + * Remove an option on th current site or the current network. + * + * @param bool $network Whether to delete a network-wide option. + * @param string $option Name of option. Expected to not be SQL-escaped. + * + * @return bool False if value was not deleted. True if value was deleted. + */ +function delete_self_option( bool $network, string $option ): bool { + return $network ? delete_site_option( $option ) : delete_option( $option ); +} + /** * Returns 'true' if plugin settings are unified on a multisite installation * under the Network Admin settings menu From ddfe1441f4b29a089feadcade056cd256a1afcb0 Mon Sep 17 00:00:00 2001 From: Shea Bunge Date: Fri, 1 Aug 2025 19:17:38 +1000 Subject: [PATCH 061/318] Implement community cloud search page. --- eslint.config.mjs | 3 +- src/css/common/_banners.scss | 43 ++++++ src/css/common/_theme.scss | 3 + src/css/common/_toolbar.scss | 5 +- src/css/edit.scss | 2 +- src/css/edit/_gpt.scss | 2 +- src/css/manage-legacy.scss | 2 +- src/css/manage.scss | 18 +++ src/css/manage/_cloud-community.scss | 141 +++++++++++++++++ src/css/manage/_snippets-table.scss | 143 ++++++++++++++++++ src/css/welcome.scss | 12 +- .../ConditionModal/ConditionModalButton.tsx | 8 +- src/js/components/EditMenu/EditMenu.tsx | 5 + .../EditorSidebar/EditorSidebar.tsx | 8 +- .../EditorSidebar/actions/ExportButtons.tsx | 12 +- .../EditorSidebar/actions/ShortcodeInfo.tsx | 4 +- .../EditorSidebar/actions/SubmitButtons.tsx | 12 +- .../controls/ActivationSwitch.tsx | 6 +- .../controls/MultisiteSharingSettings.tsx | 2 +- .../EditorSidebar/controls/PriorityInput.tsx | 4 +- .../EditorSidebar/controls/RTLControl.tsx | 2 +- .../EditorSidebar/controls/TagsInput.tsx | 2 +- .../{ => EditMenu}/EditorSidebar/index.ts | 0 .../SnippetForm/SnippetForm.tsx | 22 +-- .../SnippetForm/fields/CodeEditor.tsx | 14 +- .../fields/CodeEditorShortcuts.tsx | 36 ++--- .../SnippetForm/fields/DescriptionEditor.tsx | 2 +- .../SnippetForm/fields/NameInput.tsx | 2 +- .../fields/SnippetLocationInput.tsx | 10 +- .../SnippetForm/fields/SnippetTypeInput.tsx | 14 +- .../{ => EditMenu}/SnippetForm/index.ts | 0 .../SnippetForm/page/Notices.tsx | 4 +- .../SnippetForm/page/PageHeading.tsx | 6 +- src/js/components/EditMenu/index.ts | 1 + .../ManageMenu/CommunityCloud/CloudSearch.tsx | 118 +++++++++++++++ .../CommunityCloud/CommunityCloud.tsx | 60 ++++++++ .../CommunityCloud/SearchFilters.tsx | 101 +++++++++++++ .../CommunityCloud/SearchResults.tsx | 100 ++++++++++++ src/js/components/ManageMenu/ManageMenu.tsx | 19 +++ .../SnippetsTable/SnippetsListTable.tsx | 27 ++-- .../SnippetsTable/SnippetsTable.tsx | 26 ++-- .../SnippetsTable/TableColumns.tsx | 26 ++-- .../{ => ManageMenu}/SnippetsTable/index.ts | 0 src/js/components/ManageMenu/index.ts | 1 + .../Changelog.tsx | 0 .../WelcomeMenu.tsx} | 2 +- src/js/components/WelcomeMenu/index.ts | 1 + src/js/components/WelcomePage/index.ts | 1 - .../components/common/DismissableNotice.tsx | 4 +- .../components/common/ListTable/ListTable.tsx | 16 +- .../components/common/ListTable/TableNav.tsx | 16 +- .../common/ListTable/TablePagination.tsx | 17 +-- src/js/components/common/Toolbar.tsx | 7 +- src/js/entries/edit.ts | 4 +- src/js/entries/manage.ts | 4 +- src/js/entries/welcome.ts | 4 +- src/js/hooks/useCloudSearch.tsx | 72 +++++++++ src/js/hooks/useRestAPI.tsx | 20 ++- src/js/services/manage/requests.ts | 13 +- src/js/types/Window.ts | 1 + src/js/types/schema/CloudSnippetSchema.ts | 29 ++++ src/js/utils/Prism.ts | 17 +++ src/js/utils/bootstrap.tsx | 1 - src/js/utils/restAPI.ts | 1 + src/php/admin-menus/class-edit-menu.php | 2 +- src/php/admin-menus/class-manage-menu.php | 2 +- src/php/class-admin.php | 4 +- src/php/class-plugin.php | 12 +- .../class-cloud-snippets-rest-controller.php | 128 ++++++++++++++++ src/php/rest-api/class-rest-api.php | 5 + src/php/views/manage.php | 2 +- src/php/views/partials/list-table-notices.php | 4 +- 72 files changed, 1217 insertions(+), 200 deletions(-) create mode 100644 src/css/common/_banners.scss create mode 100644 src/css/manage/_cloud-community.scss create mode 100644 src/css/manage/_snippets-table.scss rename src/js/components/{ => EditMenu}/ConditionModal/ConditionModalButton.tsx (84%) create mode 100644 src/js/components/EditMenu/EditMenu.tsx rename src/js/components/{ => EditMenu}/EditorSidebar/EditorSidebar.tsx (87%) rename src/js/components/{ => EditMenu}/EditorSidebar/actions/ExportButtons.tsx (78%) rename src/js/components/{ => EditMenu}/EditorSidebar/actions/ShortcodeInfo.tsx (95%) rename src/js/components/{ => EditMenu}/EditorSidebar/actions/SubmitButtons.tsx (82%) rename src/js/components/{ => EditMenu}/EditorSidebar/controls/ActivationSwitch.tsx (85%) rename src/js/components/{ => EditMenu}/EditorSidebar/controls/MultisiteSharingSettings.tsx (93%) rename src/js/components/{ => EditMenu}/EditorSidebar/controls/PriorityInput.tsx (86%) rename src/js/components/{ => EditMenu}/EditorSidebar/controls/RTLControl.tsx (90%) rename src/js/components/{ => EditMenu}/EditorSidebar/controls/TagsInput.tsx (92%) rename src/js/components/{ => EditMenu}/EditorSidebar/index.ts (100%) rename src/js/components/{ => EditMenu}/SnippetForm/SnippetForm.tsx (90%) rename src/js/components/{ => EditMenu}/SnippetForm/fields/CodeEditor.tsx (86%) rename src/js/components/{ => EditMenu}/SnippetForm/fields/CodeEditorShortcuts.tsx (90%) rename src/js/components/{ => EditMenu}/SnippetForm/fields/DescriptionEditor.tsx (96%) rename src/js/components/{ => EditMenu}/SnippetForm/fields/NameInput.tsx (91%) rename src/js/components/{ => EditMenu}/SnippetForm/fields/SnippetLocationInput.tsx (87%) rename src/js/components/{ => EditMenu}/SnippetForm/fields/SnippetTypeInput.tsx (86%) rename src/js/components/{ => EditMenu}/SnippetForm/index.ts (100%) rename src/js/components/{ => EditMenu}/SnippetForm/page/Notices.tsx (86%) rename src/js/components/{ => EditMenu}/SnippetForm/page/PageHeading.tsx (88%) create mode 100644 src/js/components/EditMenu/index.ts create mode 100644 src/js/components/ManageMenu/CommunityCloud/CloudSearch.tsx create mode 100644 src/js/components/ManageMenu/CommunityCloud/CommunityCloud.tsx create mode 100644 src/js/components/ManageMenu/CommunityCloud/SearchFilters.tsx create mode 100644 src/js/components/ManageMenu/CommunityCloud/SearchResults.tsx create mode 100644 src/js/components/ManageMenu/ManageMenu.tsx rename src/js/components/{ => ManageMenu}/SnippetsTable/SnippetsListTable.tsx (85%) rename src/js/components/{ => ManageMenu}/SnippetsTable/SnippetsTable.tsx (82%) rename src/js/components/{ => ManageMenu}/SnippetsTable/TableColumns.tsx (89%) rename src/js/components/{ => ManageMenu}/SnippetsTable/index.ts (100%) create mode 100644 src/js/components/ManageMenu/index.ts rename src/js/components/{WelcomePage => WelcomeMenu}/Changelog.tsx (100%) rename src/js/components/{WelcomePage/WelcomePage.tsx => WelcomeMenu/WelcomeMenu.tsx} (98%) create mode 100644 src/js/components/WelcomeMenu/index.ts delete mode 100644 src/js/components/WelcomePage/index.ts create mode 100644 src/js/hooks/useCloudSearch.tsx create mode 100644 src/js/types/schema/CloudSnippetSchema.ts create mode 100644 src/js/utils/Prism.ts create mode 100644 src/php/rest-api/class-cloud-snippets-rest-controller.php diff --git a/eslint.config.mjs b/eslint.config.mjs index 6360acc5..2a7a1dd6 100755 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -81,11 +81,11 @@ export default eslintTs.config( objectLiteralTypeAssertions: 'never' }], '@typescript-eslint/consistent-type-imports': 'error', - '@typescript-eslint/consistent-type-exports': 'error', '@typescript-eslint/no-confusing-void-expression': ['error', { ignoreArrowShorthand: true }], '@typescript-eslint/no-for-in-array': 'error', '@typescript-eslint/no-import-type-side-effects': 'error', '@typescript-eslint/no-inferrable-types': ['error', { ignoreProperties: true, ignoreParameters: false }], + '@typescript-eslint/no-magic-numbers': ['error', { ignore: [-1, 0, 1], ignoreEnums: true }], '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_', @@ -117,7 +117,6 @@ export default eslintTs.config( }], 'max-lines-per-function': ['warn', { skipBlankLines: true, skipComments: true }], 'no-invalid-this': 'error', - 'no-magic-numbers': ['error', { ignore: [-1, 0, 1] }], 'no-plusplus': ['error', { allowForLoopAfterthoughts: true }], 'no-ternary': 'off', 'one-var': ['error', 'never'], diff --git a/src/css/common/_banners.scss b/src/css/common/_banners.scss new file mode 100644 index 00000000..8769e0ac --- /dev/null +++ b/src/css/common/_banners.scss @@ -0,0 +1,43 @@ +@use '../common/theme'; +@use 'sass:map'; +@use 'sass:list'; + +@mixin banners { + .banner { + border: 0; + border-radius: 5px; + display: flex; + align-items: center; + padding: 6px 10px; + gap: 8px; + margin: 0; + + .banner-dismiss { + position: unset; + margin-inline-start: auto; + padding: 0; + + &, &::before { + color: inherit; + } + } + + .wp-core-ui &.is-dismissible { + position: unset; + padding-inline-end: 10px; + } + } + + @each $name, $colors in theme.$notices { + .banner-#{$name} { + color: list.nth($colors, 2); + background-color: list.nth($colors, 1); + } + } + + .banner-success::before { + content: '✓'; + font-weight: bold; + font-size: 16px; + } +} diff --git a/src/css/common/_theme.scss b/src/css/common/_theme.scss index 4cb4346c..7ef72fe2 100644 --- a/src/css/common/_theme.scss +++ b/src/css/common/_theme.scss @@ -33,6 +33,9 @@ $notices: ( success: #d3e9d3 #377a37, warning: #f2ebc3 #b0730a, error: #f8d7da #721c24, + info: #d2e6f4 #2b71a3, + neutral: #e2e5e5 #6c7e7e, + special: #dfc5ef #6e249c ); @function contrasting-text-color($bg-color) { diff --git a/src/css/common/_toolbar.scss b/src/css/common/_toolbar.scss index d44805d8..676f7197 100644 --- a/src/css/common/_toolbar.scss +++ b/src/css/common/_toolbar.scss @@ -49,13 +49,13 @@ $wpbody-block-indent: 20px; line-height: 1.5; } - nav ul { + ul { display: flex; gap: 22px; align-items: center; } - nav a { + a { color: inherit; text-decoration: none; @@ -86,6 +86,7 @@ $wpbody-block-indent: 20px; box-sizing: border-box; transition: border unset; border-block-end: 4px solid transparent; + cursor: pointer; &.active-link, &:hover, &:focus, &:active { color: #2271b1; diff --git a/src/css/edit.scss b/src/css/edit.scss index f531e8ee..ad7d59d5 100644 --- a/src/css/edit.scss +++ b/src/css/edit.scss @@ -15,7 +15,7 @@ @use 'edit/conditions'; @use 'edit/gpt'; -.notice.error blockquote { +.banner.error blockquote { margin-block-end: 0; } diff --git a/src/css/edit/_gpt.scss b/src/css/edit/_gpt.scss index 9adb6802..32be7650 100644 --- a/src/css/edit/_gpt.scss +++ b/src/css/edit/_gpt.scss @@ -12,7 +12,7 @@ box-shadow: none; } - .notice { + .banner { margin-inline: 0; } } diff --git a/src/css/manage-legacy.scss b/src/css/manage-legacy.scss index ab5f4ed2..95b8f7ef 100644 --- a/src/css/manage-legacy.scss +++ b/src/css/manage-legacy.scss @@ -170,7 +170,7 @@ td.column-description { padding: 1em 0; } -.code-snippets-notice a.notice-dismiss { +.code-snippets-notice a.banner-dismiss { text-decoration: none; } diff --git a/src/css/manage.scss b/src/css/manage.scss index acafcccd..daf2ba16 100644 --- a/src/css/manage.scss +++ b/src/css/manage.scss @@ -4,6 +4,24 @@ @use 'common/select'; @use 'common/upsell'; @use 'common/toolbar'; +@use 'prism'; +@use 'manage/snippets-table'; +@use 'manage/cloud-community'; + +.snippets-page-heading { + display: flex; + align-items: center; + margin-block: 50px 1.4rem; + + h1 { + font-size: 1.6rem; + padding: 0; + } + + .button-primary { + margin-inline-start: auto; + } +} .nav-tab { display: flex; diff --git a/src/css/manage/_cloud-community.scss b/src/css/manage/_cloud-community.scss new file mode 100644 index 00000000..47f12057 --- /dev/null +++ b/src/css/manage/_cloud-community.scss @@ -0,0 +1,141 @@ +@use '../common/theme'; +@use '../common/banners'; + +.cloud-search { + @include banners.banners; + + .banner { + justify-content: center; + } +} + +.cloud-search-form { + display: flex; + gap: 8px; + margin-block: 31px 47px; + block-size: 54px; + + select { + flex: 0 0 250px; + } + + .button { + flex: 0 0 165px; + } + + .cloud-search-query { + flex: 1; + position: relative; + + input { + inline-size: 100%; + block-size: 100%; + } + + .components-spinner { + position: absolute; + inset-inline-end: 1.5em; + inset-block-start: 25%; + } + } +} + +.tablenav.top { + display: flex; + gap: 20px; + block-size: 40px; + margin-block-end: 31px; + align-items: center; + + select { + inline-size: 245px; + block-size: 100%; + } + + .tablenav-pages { + margin-inline-start: auto; + } +} + +.cloud-search-results { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(580px, 1fr)); + gap: 20px; +} + +.cloud-search-result { + background: #fff; + border: 1px solid #c3c4c7; + display: flex; + flex-flow: column; + box-sizing: border-box; + + a { + text-decoration: none; + } + + p:last-child { + margin-block-end: 0; + } + + .cloud-snippet { + padding: 24px; + } + + .cloud-snippet-meta { + display: flex; + gap: 16px; + } + + footer { + display: flex; + gap: 8px; + background: #f6f7f7; + margin-block-start: auto; + border-block-start: 1px solid #c3c4c7; + padding-inline: 24px; + padding-block: 12px; + align-items: center; + + .button:first-of-type { + margin-inline-start: auto; + } + } + + .cloud-snippet-votes { + .dashicons { + color: theme.$accent; + padding-inline-end: 8px; + } + } + + $status-colors: ( + public #64baba, + private #cc96fb, + unverified #ea835e, + ai-verified #1cabcf, + pro-verified #7cd68a, + ); + + .cloud-snippet-status { + text-transform: uppercase; + color: #646970; + font-size: 12px; + font-weight: 700; + + &::before { + content: ''; + block-size: 10px; + inline-size: 10px; + border-radius: 50%; + display: inline-block; + margin-inline-end: 8px; + } + } + + @each $status, $color in $status-colors { + .cloud-snippet-status-#{$status}::before { + background: $color; + } + } +} diff --git a/src/css/manage/_snippets-table.scss b/src/css/manage/_snippets-table.scss new file mode 100644 index 00000000..b0cdbc9b --- /dev/null +++ b/src/css/manage/_snippets-table.scss @@ -0,0 +1,143 @@ +@use '../common/theme'; + +.name-column, +.type-column { + .dashicons { + font-size: 16px; + inline-size: 16px; + block-size: 16px; + vertical-align: middle; + } + + .dashicons-clock { + vertical-align: middle; + } +} + +.active-snippet .name-column > a { + font-weight: 600; +} + +.active-snippet { + td, th { + background-color: rgba(#78c8e6, 0.06); + } + + th.check-column { + border-inline-start: 2px solid #2ea2cc; + } +} + +.priority-column input { + appearance: none; + background: none; + border: none; + box-shadow: none; + inline-size: 4em; + color: #666; + text-align: center; + + &:hover, &:focus, &:active { + color: #000; + background-color: rgb(0 0 0 / 10%); + border-radius: 6px; + appearance: unset; + } + + &:disabled { + color: inherit; + } +} + +.wp-list-table { + td.column-id { + text-align: center; + } + + tr { + background: #fff; + } + + ol, ul { + margin-block: 0 1.5em; + margin-inline: 1.5em 0; + } + + ul { + list-style: disc; + } + + th.sortable a, th.sorted a { + display: flex; + flex-direction: row; + } + + .row-actions { + color: #ddd; + position: relative; + inset-inline-start: 0; + + .button-link { + min-block-size: unset; + line-height: unset; + + &:hover { + background: none; + } + } + } + + .column-activate { + padding-inline-end: 0 !important; + } + + .clear-filters { + vertical-align: middle; + } + + tfoot th.check-column { + padding: 13px 0 0 3px; + } + + thead th.check-column, + tfoot th.check-column, + .inactive-snippet th.check-column { + padding-inline-start: 5px; + } + + .active-snippet, .inactive-snippet { + td, th { + padding: 10px 9px; + border: none; + box-shadow: inset 0 -1px 0 rgb(0 0 0 / 10%); + } + } + + tr.active-snippet + tr.inactive-snippet th, + tr.active-snippet + tr.inactive-snippet td { + border-block-start: 1px solid rgb(0 0 0 / 3%); + box-shadow: inset 0 1px 0 rgb(0 0 0 / 2%), inset 0 -1px 0 #e1e1e1; + } + + .delete { + color: #b32d2e; + + &:hover, &:focus, &:active { + border-block-end: 1px solid #f00; + color: #f00; + } + } + + #wpbody-content & .column-name { + white-space: nowrap; /* prevents wrapping of snippet title */ + vertical-align: top; + } +} + +.wp-core-ui .button.clear-filters { + vertical-align: baseline; +} + +.inactive-snippet { + @include theme.link-colors(#579); +} diff --git a/src/css/welcome.scss b/src/css/welcome.scss index 8a67c926..80e97def 100644 --- a/src/css/welcome.scss +++ b/src/css/welcome.scss @@ -74,13 +74,13 @@ $breakpoint: 1060px; figure { margin: 0; padding: 0; - } - img { - inline-size: 100%; - block-size: 220px; - overflow: hidden; - object-fit: cover; + img { + inline-size: 100%; + block-size: 220px; + overflow: hidden; + object-fit: cover; + } } } diff --git a/src/js/components/ConditionModal/ConditionModalButton.tsx b/src/js/components/EditMenu/ConditionModal/ConditionModalButton.tsx similarity index 84% rename from src/js/components/ConditionModal/ConditionModalButton.tsx rename to src/js/components/EditMenu/ConditionModal/ConditionModalButton.tsx index b1f9ccb2..ae1dedb9 100644 --- a/src/js/components/ConditionModal/ConditionModalButton.tsx +++ b/src/js/components/EditMenu/ConditionModal/ConditionModalButton.tsx @@ -1,10 +1,10 @@ import React from 'react' import classnames from 'classnames' import { __ } from '@wordpress/i18n' -import { isLicensed } from '../../utils/screen' -import { isCondition } from '../../utils/snippets/snippets' -import { Button } from '../common/Button' -import { useSnippetForm } from '../../hooks/useSnippetForm' +import { isLicensed } from '../../../utils/screen' +import { isCondition } from '../../../utils/snippets/snippets' +import { Button } from '../../common/Button' +import { useSnippetForm } from '../../../hooks/useSnippetForm' import type { Dispatch, SetStateAction } from 'react' export interface ConditionModalButtonProps { diff --git a/src/js/components/EditMenu/EditMenu.tsx b/src/js/components/EditMenu/EditMenu.tsx new file mode 100644 index 00000000..2272559a --- /dev/null +++ b/src/js/components/EditMenu/EditMenu.tsx @@ -0,0 +1,5 @@ +import React from 'react' +import { SnippetForm } from './SnippetForm' + +export const EditMenu = () => + diff --git a/src/js/components/EditorSidebar/EditorSidebar.tsx b/src/js/components/EditMenu/EditorSidebar/EditorSidebar.tsx similarity index 87% rename from src/js/components/EditorSidebar/EditorSidebar.tsx rename to src/js/components/EditMenu/EditorSidebar/EditorSidebar.tsx index 5d26b6ad..4d40d2ed 100644 --- a/src/js/components/EditorSidebar/EditorSidebar.tsx +++ b/src/js/components/EditMenu/EditorSidebar/EditorSidebar.tsx @@ -2,10 +2,10 @@ import React from 'react' import { Spinner } from '@wordpress/components' import { __, isRTL } from '@wordpress/i18n' import { addQueryArgs } from '@wordpress/url' -import { useSnippetForm } from '../../hooks/useSnippetForm' -import { isNetworkAdmin } from '../../utils/screen' -import { getSnippetType, isCondition } from '../../utils/snippets/snippets' -import { DeleteButton } from '../common/DeleteButton' +import { useSnippetForm } from '../../../hooks/useSnippetForm' +import { isNetworkAdmin } from '../../../utils/screen' +import { getSnippetType, isCondition } from '../../../utils/snippets/snippets' +import { DeleteButton } from '../../common/DeleteButton' import { Notices } from '../SnippetForm/page/Notices' import { ShortcodeInfo } from './actions/ShortcodeInfo' import { MultisiteSharingSettings } from './controls/MultisiteSharingSettings' diff --git a/src/js/components/EditorSidebar/actions/ExportButtons.tsx b/src/js/components/EditMenu/EditorSidebar/actions/ExportButtons.tsx similarity index 78% rename from src/js/components/EditorSidebar/actions/ExportButtons.tsx rename to src/js/components/EditMenu/EditorSidebar/actions/ExportButtons.tsx index eb2cfa39..adc7097f 100644 --- a/src/js/components/EditorSidebar/actions/ExportButtons.tsx +++ b/src/js/components/EditMenu/EditorSidebar/actions/ExportButtons.tsx @@ -1,11 +1,11 @@ import React from 'react' import { __ } from '@wordpress/i18n' -import { useRestAPI } from '../../../hooks/useRestAPI' -import { downloadSnippetExportFile } from '../../../utils/files' -import { Button } from '../../common/Button' -import { useSnippetForm } from '../../../hooks/useSnippetForm' -import type { Snippet } from '../../../types/Snippet' -import type { SnippetsExport } from '../../../types/schema/SnippetsExport' +import { useRestAPI } from '../../../../hooks/useRestAPI' +import { useSnippetForm } from '../../../../hooks/useSnippetForm' +import { SnippetsExport } from '../../../../types/schema/SnippetsExport' +import { Snippet } from '../../../../types/Snippet' +import { downloadSnippetExportFile } from '../../../../utils/files' +import { Button } from '../../../common/Button' interface ExportButtonProps { name: string diff --git a/src/js/components/EditorSidebar/actions/ShortcodeInfo.tsx b/src/js/components/EditMenu/EditorSidebar/actions/ShortcodeInfo.tsx similarity index 95% rename from src/js/components/EditorSidebar/actions/ShortcodeInfo.tsx rename to src/js/components/EditMenu/EditorSidebar/actions/ShortcodeInfo.tsx index 2d1ad820..ebb956be 100644 --- a/src/js/components/EditorSidebar/actions/ShortcodeInfo.tsx +++ b/src/js/components/EditMenu/EditorSidebar/actions/ShortcodeInfo.tsx @@ -1,8 +1,8 @@ import React, { useState } from 'react' import { ExternalLink } from '@wordpress/components' import { __ } from '@wordpress/i18n' -import { useSnippetForm } from '../../../hooks/useSnippetForm' -import { CopyToClipboardButton } from '../../common/CopyToClipboardButton' +import { useSnippetForm } from '../../../../hooks/useSnippetForm' +import { CopyToClipboardButton } from '../../../common/CopyToClipboardButton' import type { Dispatch, SetStateAction} from 'react' type ShortcodeAtts = Record diff --git a/src/js/components/EditorSidebar/actions/SubmitButtons.tsx b/src/js/components/EditMenu/EditorSidebar/actions/SubmitButtons.tsx similarity index 82% rename from src/js/components/EditorSidebar/actions/SubmitButtons.tsx rename to src/js/components/EditMenu/EditorSidebar/actions/SubmitButtons.tsx index 01167a09..328338c2 100644 --- a/src/js/components/EditorSidebar/actions/SubmitButtons.tsx +++ b/src/js/components/EditMenu/EditorSidebar/actions/SubmitButtons.tsx @@ -1,11 +1,11 @@ import React from 'react' import { __ } from '@wordpress/i18n' -import { SubmitSnippetAction } from '../../../hooks/useSubmitSnippet' -import { isCondition } from '../../../utils/snippets/snippets' -import { isNetworkAdmin } from '../../../utils/screen' -import { useSnippetForm } from '../../../hooks/useSnippetForm' -import { SubmitButton } from '../../common/SubmitButton' -import type { SubmitButtonProps } from '../../common/SubmitButton' +import { SubmitSnippetAction } from '../../../../hooks/useSubmitSnippet' +import { isCondition } from '../../../../utils/snippets/snippets' +import { isNetworkAdmin } from '../../../../utils/screen' +import { useSnippetForm } from '../../../../hooks/useSnippetForm' +import { SubmitButton } from '../../../common/SubmitButton' +import type { SubmitButtonProps } from '../../../common/SubmitButton' const SaveButton = (props: SubmitButtonProps) => { const { snippet } = useSnippetForm() diff --git a/src/js/components/EditorSidebar/controls/ActivationSwitch.tsx b/src/js/components/EditMenu/EditorSidebar/controls/ActivationSwitch.tsx similarity index 85% rename from src/js/components/EditorSidebar/controls/ActivationSwitch.tsx rename to src/js/components/EditMenu/EditorSidebar/controls/ActivationSwitch.tsx index 00f2d301..79f8e4b1 100644 --- a/src/js/components/EditorSidebar/controls/ActivationSwitch.tsx +++ b/src/js/components/EditMenu/EditorSidebar/controls/ActivationSwitch.tsx @@ -1,8 +1,8 @@ import React from 'react' import { __ } from '@wordpress/i18n' -import { useSnippetForm } from '../../../hooks/useSnippetForm' -import { SubmitSnippetAction, useSubmitSnippet } from '../../../hooks/useSubmitSnippet' -import { handleUnknownError } from '../../../utils/errors' +import { useSnippetForm } from '../../../../hooks/useSnippetForm' +import { SubmitSnippetAction, useSubmitSnippet } from '../../../../hooks/useSubmitSnippet' +import { handleUnknownError } from '../../../../utils/errors' export const ActivationSwitch = () => { const { snippet, isWorking } = useSnippetForm() diff --git a/src/js/components/EditorSidebar/controls/MultisiteSharingSettings.tsx b/src/js/components/EditMenu/EditorSidebar/controls/MultisiteSharingSettings.tsx similarity index 93% rename from src/js/components/EditorSidebar/controls/MultisiteSharingSettings.tsx rename to src/js/components/EditMenu/EditorSidebar/controls/MultisiteSharingSettings.tsx index 27d6c57d..c976f8fc 100644 --- a/src/js/components/EditorSidebar/controls/MultisiteSharingSettings.tsx +++ b/src/js/components/EditMenu/EditorSidebar/controls/MultisiteSharingSettings.tsx @@ -1,6 +1,6 @@ import React from 'react' import { __ } from '@wordpress/i18n' -import { useSnippetForm } from '../../../hooks/useSnippetForm' +import { useSnippetForm } from '../../../../hooks/useSnippetForm' export const MultisiteSharingSettings: React.FC = () => { const { snippet, setSnippet, isReadOnly } = useSnippetForm() diff --git a/src/js/components/EditorSidebar/controls/PriorityInput.tsx b/src/js/components/EditMenu/EditorSidebar/controls/PriorityInput.tsx similarity index 86% rename from src/js/components/EditorSidebar/controls/PriorityInput.tsx rename to src/js/components/EditMenu/EditorSidebar/controls/PriorityInput.tsx index d6ae07e2..45ddf732 100644 --- a/src/js/components/EditorSidebar/controls/PriorityInput.tsx +++ b/src/js/components/EditMenu/EditorSidebar/controls/PriorityInput.tsx @@ -1,7 +1,7 @@ import React from 'react' import { __, _x } from '@wordpress/i18n' -import { useSnippetForm } from '../../../hooks/useSnippetForm' -import { Tooltip } from '../../common/Tooltip' +import { useSnippetForm } from '../../../../hooks/useSnippetForm' +import { Tooltip } from '../../../common/Tooltip' export const PriorityInput = () => { const { snippet, isReadOnly, setSnippet } = useSnippetForm() diff --git a/src/js/components/EditorSidebar/controls/RTLControl.tsx b/src/js/components/EditMenu/EditorSidebar/controls/RTLControl.tsx similarity index 90% rename from src/js/components/EditorSidebar/controls/RTLControl.tsx rename to src/js/components/EditMenu/EditorSidebar/controls/RTLControl.tsx index 64496508..531579d3 100644 --- a/src/js/components/EditorSidebar/controls/RTLControl.tsx +++ b/src/js/components/EditMenu/EditorSidebar/controls/RTLControl.tsx @@ -1,6 +1,6 @@ import React from 'react' import { __ } from '@wordpress/i18n' -import { useSnippetForm } from '../../../hooks/useSnippetForm' +import { useSnippetForm } from '../../../../hooks/useSnippetForm' export const RTLControl: React.FC = () => { const { codeEditorInstance } = useSnippetForm() diff --git a/src/js/components/EditorSidebar/controls/TagsInput.tsx b/src/js/components/EditMenu/EditorSidebar/controls/TagsInput.tsx similarity index 92% rename from src/js/components/EditorSidebar/controls/TagsInput.tsx rename to src/js/components/EditMenu/EditorSidebar/controls/TagsInput.tsx index d92a171f..a960b85b 100644 --- a/src/js/components/EditorSidebar/controls/TagsInput.tsx +++ b/src/js/components/EditMenu/EditorSidebar/controls/TagsInput.tsx @@ -1,7 +1,7 @@ import React from 'react' import { __, _x } from '@wordpress/i18n' import { FormTokenField } from '@wordpress/components' -import { useSnippetForm } from '../../../hooks/useSnippetForm' +import { useSnippetForm } from '../../../../hooks/useSnippetForm' const options = window.CODE_SNIPPETS_EDIT?.tagOptions diff --git a/src/js/components/EditorSidebar/index.ts b/src/js/components/EditMenu/EditorSidebar/index.ts similarity index 100% rename from src/js/components/EditorSidebar/index.ts rename to src/js/components/EditMenu/EditorSidebar/index.ts diff --git a/src/js/components/SnippetForm/SnippetForm.tsx b/src/js/components/EditMenu/SnippetForm/SnippetForm.tsx similarity index 90% rename from src/js/components/SnippetForm/SnippetForm.tsx rename to src/js/components/EditMenu/SnippetForm/SnippetForm.tsx index c9462e03..b5ccedaf 100644 --- a/src/js/components/SnippetForm/SnippetForm.tsx +++ b/src/js/components/EditMenu/SnippetForm/SnippetForm.tsx @@ -2,18 +2,18 @@ import React, { useState } from 'react' import classnames from 'classnames' import { __ } from '@wordpress/i18n' import { addQueryArgs } from '@wordpress/url' -import { WithRestAPIContext } from '../../hooks/useRestAPI' -import { WithSnippetsListContext, useSnippetsList } from '../../hooks/useSnippetsList' -import { SubmitSnippetAction, useSubmitSnippet } from '../../hooks/useSubmitSnippet' -import { handleUnknownError } from '../../utils/errors' -import { createSnippetObject, getSnippetType, isCondition, validateSnippet } from '../../utils/snippets/snippets' -import { WithSnippetFormContext, useSnippetForm } from '../../hooks/useSnippetForm' -import { ConfirmDialog } from '../common/ConfirmDialog' -import { Toolbar } from '../common/Toolbar' -import { UpsellDialog } from '../common/UpsellDialog' +import { WithRestAPIContext } from '../../../hooks/useRestAPI' +import { WithSnippetsListContext, useSnippetsList } from '../../../hooks/useSnippetsList' +import { SubmitSnippetAction, useSubmitSnippet } from '../../../hooks/useSubmitSnippet' +import { handleUnknownError } from '../../../utils/errors' +import { createSnippetObject, getSnippetType, isCondition, validateSnippet } from '../../../utils/snippets/snippets' +import { WithSnippetFormContext, useSnippetForm } from '../../../hooks/useSnippetForm' +import { ConfirmDialog } from '../../common/ConfirmDialog' +import { Toolbar } from '../../common/Toolbar' +import { UpsellDialog } from '../../common/UpsellDialog' import { ConditionModalButton } from '../ConditionModal/ConditionModalButton' import { EditorSidebar } from '../EditorSidebar' -import { UpsellBanner } from '../common/UpsellBanner' +import { UpsellBanner } from '../../common/UpsellBanner' import { CodeEditor } from './fields/CodeEditor' import { SnippetLocationInput } from './fields/SnippetLocationInput' import { SnippetTypeInput } from './fields/SnippetTypeInput' @@ -21,7 +21,7 @@ import { DescriptionEditor } from './fields/DescriptionEditor' import { NameInput } from './fields/NameInput' import { PageHeading } from './page/PageHeading' import type { PropsWithChildren } from 'react' -import type { Snippet } from '../../types/Snippet' +import type { Snippet } from '../../../types/Snippet' const editFormClassName = ({ snippet, isReadOnly, isExpanded }: { snippet: Snippet, diff --git a/src/js/components/SnippetForm/fields/CodeEditor.tsx b/src/js/components/EditMenu/SnippetForm/fields/CodeEditor.tsx similarity index 86% rename from src/js/components/SnippetForm/fields/CodeEditor.tsx rename to src/js/components/EditMenu/SnippetForm/fields/CodeEditor.tsx index 027b19c8..e2643e63 100644 --- a/src/js/components/SnippetForm/fields/CodeEditor.tsx +++ b/src/js/components/EditMenu/SnippetForm/fields/CodeEditor.tsx @@ -1,12 +1,12 @@ import React, { useEffect, useRef } from 'react' import { __ } from '@wordpress/i18n' -import { useSubmitSnippet } from '../../../hooks/useSubmitSnippet' -import { handleUnknownError } from '../../../utils/errors' -import { isMacOS } from '../../../utils/screen' -import { useSnippetForm } from '../../../hooks/useSnippetForm' -import { Button } from '../../common/Button' -import { ExpandIcon } from '../../common/icons/ExpandIcon' -import { MinimiseIcon } from '../../common/icons/MinimiseIcon' +import { useSubmitSnippet } from '../../../../hooks/useSubmitSnippet' +import { handleUnknownError } from '../../../../utils/errors' +import { isMacOS } from '../../../../utils/screen' +import { useSnippetForm } from '../../../../hooks/useSnippetForm' +import { Button } from '../../../common/Button' +import { ExpandIcon } from '../../../common/icons/ExpandIcon' +import { MinimiseIcon } from '../../../common/icons/MinimiseIcon' import { CodeEditorShortcuts } from './CodeEditorShortcuts' import type { Dispatch, RefObject, SetStateAction } from 'react' diff --git a/src/js/components/SnippetForm/fields/CodeEditorShortcuts.tsx b/src/js/components/EditMenu/SnippetForm/fields/CodeEditorShortcuts.tsx similarity index 90% rename from src/js/components/SnippetForm/fields/CodeEditorShortcuts.tsx rename to src/js/components/EditMenu/SnippetForm/fields/CodeEditorShortcuts.tsx index 6ee4fb09..5c934911 100644 --- a/src/js/components/SnippetForm/fields/CodeEditorShortcuts.tsx +++ b/src/js/components/EditMenu/SnippetForm/fields/CodeEditorShortcuts.tsx @@ -1,9 +1,9 @@ import { __, _x } from '@wordpress/i18n' -import React, { Fragment, useMemo } from 'react' import { getKeyMap } from 'codemirror/src/input/keymap' -import { useSnippetForm } from '../../../hooks/useSnippetForm' -import { isMacOS } from '../../../utils/screen' -import type { KeyMap } from 'codemirror' +import { useSnippetForm } from '../../../../hooks/useSnippetForm' +import { isMacOS } from '../../../../utils/screen' +import { KeyMap } from 'codemirror' +import React, { Fragment, useMemo } from 'react' const KEYBOARD_KEYS = { 'Fn': _x('Fn', 'keyboard key', 'code-snippets'), @@ -146,22 +146,22 @@ export const CodeEditorShortcuts: React.FC = ({ editor const { codeEditorInstance } = useSnippetForm() const shortcutKeys: Map | undefined = useMemo(() => { - if (codeEditorInstance) { - const extraKeys = codeEditorInstance.codemirror.getOption('extraKeys') - const keyMapName = codeEditorInstance.codemirror.getOption('keyMap') - - const combinedKeyMap: KeyMap = { - ...isMacOS() ? fallbackKeyMapMac : fallbackKeyMap, - ...keyMapName && getKeyMap(keyMapName), - ...'object' === typeof extraKeys ? extraKeys : undefined - } + if (codeEditorInstance) { + const extraKeys = codeEditorInstance.codemirror.getOption('extraKeys') + const keyMapName = codeEditorInstance.codemirror.getOption('keyMap') - return unpackKeyMap(combinedKeyMap) - } + const combinedKeyMap: KeyMap = { + ...isMacOS() ? fallbackKeyMapMac : fallbackKeyMap, + ...keyMapName && getKeyMap(keyMapName), + ...'object' === typeof extraKeys ? extraKeys : undefined + } + + return unpackKeyMap(combinedKeyMap) + } - return undefined - }, - [codeEditorInstance] + return undefined + }, + [codeEditorInstance] ) return shortcutKeys diff --git a/src/js/components/SnippetForm/fields/DescriptionEditor.tsx b/src/js/components/EditMenu/SnippetForm/fields/DescriptionEditor.tsx similarity index 96% rename from src/js/components/SnippetForm/fields/DescriptionEditor.tsx rename to src/js/components/EditMenu/SnippetForm/fields/DescriptionEditor.tsx index 65963508..8e590d72 100644 --- a/src/js/components/SnippetForm/fields/DescriptionEditor.tsx +++ b/src/js/components/EditMenu/SnippetForm/fields/DescriptionEditor.tsx @@ -1,7 +1,7 @@ import React, { useCallback, useEffect } from 'react' import { __ } from '@wordpress/i18n' import domReady from '@wordpress/dom-ready' -import { useSnippetForm } from '../../../hooks/useSnippetForm' +import { useSnippetForm } from '../../../../hooks/useSnippetForm' export const EDITOR_ID = 'snippet_description' diff --git a/src/js/components/SnippetForm/fields/NameInput.tsx b/src/js/components/EditMenu/SnippetForm/fields/NameInput.tsx similarity index 91% rename from src/js/components/SnippetForm/fields/NameInput.tsx rename to src/js/components/EditMenu/SnippetForm/fields/NameInput.tsx index eb3ae0cf..0f3f7164 100644 --- a/src/js/components/SnippetForm/fields/NameInput.tsx +++ b/src/js/components/EditMenu/SnippetForm/fields/NameInput.tsx @@ -1,6 +1,6 @@ import React from 'react' import { __ } from '@wordpress/i18n' -import { useSnippetForm } from '../../../hooks/useSnippetForm' +import { useSnippetForm } from '../../../../hooks/useSnippetForm' export const NameInput: React.FC = () => { const { snippet, setSnippet, isReadOnly } = useSnippetForm() diff --git a/src/js/components/SnippetForm/fields/SnippetLocationInput.tsx b/src/js/components/EditMenu/SnippetForm/fields/SnippetLocationInput.tsx similarity index 87% rename from src/js/components/SnippetForm/fields/SnippetLocationInput.tsx rename to src/js/components/EditMenu/SnippetForm/fields/SnippetLocationInput.tsx index 532240c9..4e57787e 100644 --- a/src/js/components/SnippetForm/fields/SnippetLocationInput.tsx +++ b/src/js/components/EditMenu/SnippetForm/fields/SnippetLocationInput.tsx @@ -1,11 +1,11 @@ import { __ } from '@wordpress/i18n' import React from 'react' import Select from 'react-select' -import { useSnippetForm } from '../../../hooks/useSnippetForm' -import { SNIPPET_TYPE_SCOPES } from '../../../types/Snippet' -import { getSnippetType, isCondition } from '../../../utils/snippets/snippets' -import type { SnippetCodeScope } from '../../../types/Snippet' -import type { SelectOption } from '../../../types/SelectOption' +import { useSnippetForm } from '../../../../hooks/useSnippetForm' +import { SNIPPET_TYPE_SCOPES } from '../../../../types/Snippet' +import { getSnippetType, isCondition } from '../../../../utils/snippets/snippets' +import type { SnippetCodeScope } from '../../../../types/Snippet' +import type { SelectOption } from '../../../../types/SelectOption' const SCOPE_ICONS: Record = { 'global': 'admin-site', diff --git a/src/js/components/SnippetForm/fields/SnippetTypeInput.tsx b/src/js/components/EditMenu/SnippetForm/fields/SnippetTypeInput.tsx similarity index 86% rename from src/js/components/SnippetForm/fields/SnippetTypeInput.tsx rename to src/js/components/EditMenu/SnippetForm/fields/SnippetTypeInput.tsx index 989b795b..73910a2c 100644 --- a/src/js/components/SnippetForm/fields/SnippetTypeInput.tsx +++ b/src/js/components/EditMenu/SnippetForm/fields/SnippetTypeInput.tsx @@ -2,14 +2,14 @@ import React, { useEffect } from 'react' import classnames from 'classnames' import { __, _x } from '@wordpress/i18n' import Select from 'react-select' -import { useSnippetForm } from '../../../hooks/useSnippetForm' -import { SNIPPET_TYPES, SNIPPET_TYPE_SCOPES } from '../../../types/Snippet' -import { isLicensed } from '../../../utils/screen' -import { SNIPPET_TYPE_LABELS, getSnippetType, isProType } from '../../../utils/snippets/snippets' -import { Badge } from '../../common/Badge' +import { useSnippetForm } from '../../../../hooks/useSnippetForm' +import { SNIPPET_TYPES, SNIPPET_TYPE_SCOPES } from '../../../../types/Snippet' +import { isLicensed } from '../../../../utils/screen' +import { SNIPPET_TYPE_LABELS, getSnippetType, isProType } from '../../../../utils/snippets/snippets' +import { Badge } from '../../../common/Badge' import type { Dispatch, SetStateAction } from 'react' -import type { SnippetCodeType, SnippetType } from '../../../types/Snippet' -import type { SelectOption } from '../../../types/SelectOption' +import type { SnippetCodeType, SnippetType } from '../../../../types/Snippet' +import type { SelectOption } from '../../../../types/SelectOption' import type { EditorConfiguration } from 'codemirror' export interface SnippetTypeInputProps { diff --git a/src/js/components/SnippetForm/index.ts b/src/js/components/EditMenu/SnippetForm/index.ts similarity index 100% rename from src/js/components/SnippetForm/index.ts rename to src/js/components/EditMenu/SnippetForm/index.ts diff --git a/src/js/components/SnippetForm/page/Notices.tsx b/src/js/components/EditMenu/SnippetForm/page/Notices.tsx similarity index 86% rename from src/js/components/SnippetForm/page/Notices.tsx rename to src/js/components/EditMenu/SnippetForm/page/Notices.tsx index 5773752b..e3bc7dc6 100644 --- a/src/js/components/SnippetForm/page/Notices.tsx +++ b/src/js/components/EditMenu/SnippetForm/page/Notices.tsx @@ -1,7 +1,7 @@ import React from 'react' import { __, sprintf } from '@wordpress/i18n' -import { useSnippetForm } from '../../../hooks/useSnippetForm' -import { DismissibleNotice } from '../../common/DismissableNotice' +import { useSnippetForm } from '../../../../hooks/useSnippetForm' +import { DismissibleNotice } from '../../../common/DismissableNotice' export const Notices: React.FC = () => { const { currentNotice, setCurrentNotice, snippet, setSnippet } = useSnippetForm() diff --git a/src/js/components/SnippetForm/page/PageHeading.tsx b/src/js/components/EditMenu/SnippetForm/page/PageHeading.tsx similarity index 88% rename from src/js/components/SnippetForm/page/PageHeading.tsx rename to src/js/components/EditMenu/SnippetForm/page/PageHeading.tsx index b769f6d9..24e0e63c 100644 --- a/src/js/components/SnippetForm/page/PageHeading.tsx +++ b/src/js/components/EditMenu/SnippetForm/page/PageHeading.tsx @@ -1,8 +1,8 @@ import { __, _x } from '@wordpress/i18n' import React from 'react' -import { useSnippetForm } from '../../../hooks/useSnippetForm' -import { createSnippetObject } from '../../../utils/snippets/snippets' -import type { Snippet } from '../../../types/Snippet' +import { useSnippetForm } from '../../../../hooks/useSnippetForm' +import { createSnippetObject } from '../../../../utils/snippets/snippets' +import type { Snippet } from '../../../../types/Snippet' const OPTIONS = window.CODE_SNIPPETS_EDIT diff --git a/src/js/components/EditMenu/index.ts b/src/js/components/EditMenu/index.ts new file mode 100644 index 00000000..39daf063 --- /dev/null +++ b/src/js/components/EditMenu/index.ts @@ -0,0 +1 @@ +export * from './EditMenu' diff --git a/src/js/components/ManageMenu/CommunityCloud/CloudSearch.tsx b/src/js/components/ManageMenu/CommunityCloud/CloudSearch.tsx new file mode 100644 index 00000000..8d277f6c --- /dev/null +++ b/src/js/components/ManageMenu/CommunityCloud/CloudSearch.tsx @@ -0,0 +1,118 @@ +import { __ } from '@wordpress/i18n' +import React, { useMemo, useState } from 'react' +import { useCloudSearch } from '../../../hooks/useCloudSearch' +import { fetchQueryParam } from '../../../utils/urls' +import { TablePagination } from '../../common/ListTable/TablePagination' +import { SubmitButton } from '../../common/SubmitButton' +import { SearchFilters } from './SearchFilters' +import { SearchResults } from './SearchResults' +import type { CloudSearchFilters } from './SearchFilters' +import type { FormEventHandler } from 'react' +import { Spinner } from '@wordpress/components' + +const SearchBox = () => { + const { query, searchByCodevault, setPage, setQuery, setSearchByCodevault, isSearching } = useCloudSearch() + + const handleSubmit: FormEventHandler = event => { + event.preventDefault() + setPage(1) + } + + return ( +
+ + + +
+ + setQuery(event.target.value)} + placeholder={__('e.g. Remove unused JavaScript…', 'code-snippets')} + /> + {isSearching && } +
+ + + + ) +} + +export const CloudSearch = () => { + const { searchResults, error, page, totalItems, totalPages, setPage } = useCloudSearch() + + const [filters, setFilters] = useState(() => { + const tags = fetchQueryParam('tags') ?? '' + const status = fetchQueryParam('status') ?? 0 + return { tags, status: Number(status) } + }) + + const filteredSearchResults = useMemo( + () => searchResults?.filter(snippet => { + console.log(snippet.status, filters.status) + + return (!filters.tags || snippet.tags.includes(filters.tags)) && + (!filters.status || snippet.status === filters.status) + }), + [searchResults, filters]) + + return ( +
+ + + {error + ?
+

{__('An error occurred while fetching search results. Please try again.')}

+
: null} + + {page > 0 && searchResults && searchResults.length === 0 + ?
+

{__('No snippets or codevault could be found with that search term. Please try again.', 'code-snippets')}

+
+ : searchResults && filteredSearchResults + ? <> +
+ + + +
+ + + +
+ +
+ + : null} +
+ ) +} diff --git a/src/js/components/ManageMenu/CommunityCloud/CommunityCloud.tsx b/src/js/components/ManageMenu/CommunityCloud/CommunityCloud.tsx new file mode 100644 index 00000000..9ce67092 --- /dev/null +++ b/src/js/components/ManageMenu/CommunityCloud/CommunityCloud.tsx @@ -0,0 +1,60 @@ +import React, { useState } from 'react' +import { __ } from '@wordpress/i18n' +import classnames from 'classnames' +import { addQueryArgs } from '@wordpress/url' +import { WithCloudSearchContext } from '../../../hooks/useCloudSearch' +import { WithRestAPIContext } from '../../../hooks/useRestAPI' +import { fetchQueryParam, updateQueryParam } from '../../../utils/urls' +import { CloudSearch } from './CloudSearch' + +const TABS = ['snippets', 'bundles'] as const +type TabName = typeof TABS[number] + +const TAB_LABELS: Record = { + snippets: __('Code Snippets', 'code-snippets'), + bundles: __('Bundles', 'code-snippets') +} + +interface NavTabsProps { + currentTab: TabName + setCurrentTab: (tab: TabName) => void +} + +const NavTabs: React.FC = ({ currentTab, setCurrentTab }) => +

+ {TABS.map(tab => + { + event.preventDefault() + updateQueryParam('tab', tab) + setCurrentTab(tab) + }} + > + {TAB_LABELS[tab]} + )} +

+ +export const CommunityCloud = () => { + const [currentTab, setCurrentTab] = useState(() => fetchQueryParam('tab') as TabName | null ?? TABS[0]) + + return ( +
+
+

{__('Community Cloud', 'code-snippets')}

+
+ + + + {'snippets' === currentTab + ? + + + + + : null} +
+ ) +} diff --git a/src/js/components/ManageMenu/CommunityCloud/SearchFilters.tsx b/src/js/components/ManageMenu/CommunityCloud/SearchFilters.tsx new file mode 100644 index 00000000..d12ce973 --- /dev/null +++ b/src/js/components/ManageMenu/CommunityCloud/SearchFilters.tsx @@ -0,0 +1,101 @@ +import React, { useMemo } from 'react' +import { __ } from '@wordpress/i18n' +import { CloudStatus } from '../../../types/schema/CloudSnippetSchema' +import { updateQueryParam } from '../../../utils/urls' +import type { CloudSnippetSchema } from '../../../types/schema/CloudSnippetSchema' +import type { Dispatch, SetStateAction } from 'react' + +export const STATUS_LABELS: Record = { + [CloudStatus.Public]: __('Public', 'code-snippets'), + [CloudStatus.Private]: __('Private', 'code-snippets'), + [CloudStatus.Unverified]: __('Unverified', 'code-snippets'), + [CloudStatus.AI_Verified]: __('AI Verified', 'code-snippets'), + [CloudStatus.Pro_Verified]: __('Pro Verified', 'code-snippets') +} + +export interface CloudSearchFilters { + tags: string + status: number +} + +interface SearchFilterProps { + label: string + filter: keyof CloudSearchFilters + filters: CloudSearchFilters + setFilters: Dispatch> + options: [string | number, string][] + allOptionLabel: string +} + +const SearchFilter: React.FC = ({ options, filter, filters, setFilters, label, allOptionLabel }) => + <> + + + + + +export interface SearchFiltersProps { + snippets: CloudSnippetSchema[] + filters: CloudSearchFilters + setFilters: Dispatch> +} + +export const SearchFilters: React.FC = ({ snippets, filters, setFilters }) => { + const options: { [K in keyof CloudSearchFilters]: [CloudSearchFilters[K], string][] } = useMemo( + () => { + const tags = new Set() + const statuses = new Set() + + snippets.forEach(snippet => { + snippet.tags.forEach(tag => tags.add(tag)) + statuses.add(snippet.status) + }) + + return { + tags: Array.from(tags).sort().map(tag => [tag, tag]), + status: Array.from(statuses).sort().map(status => [status, STATUS_LABELS[status]]) + } + }, + [snippets]) + + return ( + <> + + + + + ) +} diff --git a/src/js/components/ManageMenu/CommunityCloud/SearchResults.tsx b/src/js/components/ManageMenu/CommunityCloud/SearchResults.tsx new file mode 100644 index 00000000..5f47e315 --- /dev/null +++ b/src/js/components/ManageMenu/CommunityCloud/SearchResults.tsx @@ -0,0 +1,100 @@ +import React, { useEffect, useState } from 'react' +import classnames from 'classnames' +import { __ } from '@wordpress/i18n' +import { Modal } from '@wordpress/components' +import { CloudStatus } from '../../../types/schema/CloudSnippetSchema' +import { Prism } from '../../../utils/Prism' +import { getSnippetType } from '../../../utils/snippets/snippets' +import { Badge } from '../../common/Badge' +import { Button } from '../../common/Button' +import { STATUS_LABELS } from './SearchFilters' +import type { CloudSnippetSchema} from '../../../types/schema/CloudSnippetSchema' + +const MAX_DESCRIPTION_LENGTH = 150 + +interface SearchResultProps { + snippet: CloudSnippetSchema +} + +const SearchResult: React.FC = ({ snippet }) => { + const [isPreviewOpen, setIsPreviewOpen] = useState(false) + const snippetType = getSnippetType(snippet) + + useEffect(() => { + if (isPreviewOpen) { + Prism.highlightAll() + } + }, [isPreviewOpen]) + + return ( +
+
+

+ setIsPreviewOpen(true)} + > + {snippet.name} + +

+
+ + + + {snippet.vote_count} + + {0 < snippet.tags.length + ? + {__('Category: ', 'code-snippets')} + {snippet.tags[0]} + : null} +
+

+ {snippet.description.length > MAX_DESCRIPTION_LENGTH + ? `${snippet.description.slice(0, MAX_DESCRIPTION_LENGTH) }…` + : snippet.description} +

+

+ {__('by ', 'code-snippets')} + + {snippet.codevault} + +

+
+
+ + {STATUS_LABELS[snippet.status]} + + + +
+ + {isPreviewOpen + ? setIsPreviewOpen(false)} title={snippet.name}> +
+						
+							{'php' === snippetType ? '
+					
+
+ : null} +
+ ) +} + +interface SearchResultsProps { + results: CloudSnippetSchema[] +} + +export const SearchResults: React.FC = ({ results }) => +
+ {results.map(result => + )} +
diff --git a/src/js/components/ManageMenu/ManageMenu.tsx b/src/js/components/ManageMenu/ManageMenu.tsx new file mode 100644 index 00000000..8710cedc --- /dev/null +++ b/src/js/components/ManageMenu/ManageMenu.tsx @@ -0,0 +1,19 @@ +import React, { useMemo } from 'react' +import { fetchQueryParam } from '../../utils/urls' +import { Toolbar } from '../common/Toolbar' +import { CommunityCloud } from './CommunityCloud/CommunityCloud' +import { SnippetsTable } from './SnippetsTable' + +export const ManageMenu = () => { + const subpage = useMemo(() => fetchQueryParam('subpage'), []) + + return ( + <> + + + {'cloud-community' === subpage + ? + : } + + ) +} diff --git a/src/js/components/SnippetsTable/SnippetsListTable.tsx b/src/js/components/ManageMenu/SnippetsTable/SnippetsListTable.tsx similarity index 85% rename from src/js/components/SnippetsTable/SnippetsListTable.tsx rename to src/js/components/ManageMenu/SnippetsTable/SnippetsListTable.tsx index fdb1f09c..2d545b5d 100644 --- a/src/js/components/SnippetsTable/SnippetsListTable.tsx +++ b/src/js/components/ManageMenu/SnippetsTable/SnippetsListTable.tsx @@ -1,18 +1,18 @@ import { __, _x, sprintf } from '@wordpress/i18n' import React, { Fragment, useMemo } from 'react' import { addQueryArgs } from '@wordpress/url' -import { useFilteredSnippets } from '../../hooks/useFilteredSnippets' -import { useRestAPI } from '../../hooks/useRestAPI' -import { useSnippetsFilters } from '../../hooks/useSnippetsFilters' -import { useSnippetsList } from '../../hooks/useSnippetsList' -import { handleUnknownError } from '../../utils/errors' -import { REST_API_NAMESPACE, REST_BASE } from '../../utils/restAPI' -import { getSnippetType } from '../../utils/snippets/snippets' -import { ListTable } from '../common/ListTable' -import { SubmitButton } from '../common/SubmitButton' +import { useFilteredSnippets } from '../../../hooks/useFilteredSnippets' +import { useRestAPI } from '../../../hooks/useRestAPI' +import { useSnippetsFilters } from '../../../hooks/useSnippetsFilters' +import { useSnippetsList } from '../../../hooks/useSnippetsList' +import { handleUnknownError } from '../../../utils/errors' +import { REST_API_NAMESPACE, REST_BASE } from '../../../utils/restAPI' +import { getSnippetType } from '../../../utils/snippets/snippets' +import { ListTable } from '../../common/ListTable' +import { SubmitButton } from '../../common/SubmitButton' import { TableColumns } from './TableColumns' -import type { ListTableBulkAction } from '../common/ListTable' -import type { Snippet, SnippetStatus } from '../../types/Snippet' +import type { ListTableBulkAction } from '../../common/ListTable' +import type { Snippet, SnippetStatus } from '../../../types/Snippet' const actions: ListTableBulkAction[] = [ { @@ -171,6 +171,9 @@ export const SnippetsListTable: React.FC = () => { const { currentStatus } = useSnippetsFilters() const { snippetsByStatus } = useFilteredSnippets() + const totalItems = snippetsByStatus.get(currentStatus)?.length ?? 0 + const itemsPerPage = window.CODE_SNIPPETS_MANAGE?.snippetsPerPage + return ( <> @@ -181,7 +184,7 @@ export const SnippetsListTable: React.FC = () => { getKey={snippet => snippet.id} columns={TableColumns} actions={actions} - itemsPerPage={window.CODE_SNIPPETS_MANAGE?.snippetsPerPage} + totalPages={itemsPerPage && Math.ceil(totalItems / itemsPerPage)} extraTableNav={which => <> {'top' === which && } diff --git a/src/js/components/SnippetsTable/SnippetsTable.tsx b/src/js/components/ManageMenu/SnippetsTable/SnippetsTable.tsx similarity index 82% rename from src/js/components/SnippetsTable/SnippetsTable.tsx rename to src/js/components/ManageMenu/SnippetsTable/SnippetsTable.tsx index 5fbdc13a..451b91f8 100644 --- a/src/js/components/SnippetsTable/SnippetsTable.tsx +++ b/src/js/components/ManageMenu/SnippetsTable/SnippetsTable.tsx @@ -2,19 +2,18 @@ import { __, sprintf } from '@wordpress/i18n' import React, { useState } from 'react' import classnames from 'classnames' import { addQueryArgs } from '@wordpress/url' -import { WithFilteredSnippetsContext } from '../../hooks/useFilteredSnippets' -import { WithRestAPIContext } from '../../hooks/useRestAPI' -import { WithSnippetsListContext } from '../../hooks/useSnippetsList' -import { WithSnippetsTableFiltersContext, useSnippetsFilters } from '../../hooks/useSnippetsFilters' -import { SNIPPET_TYPES } from '../../types/Snippet' -import { isLicensed } from '../../utils/screen' -import { SNIPPET_TYPE_LABELS, getSnippetEditUrl, isProType } from '../../utils/snippets/snippets' -import { Badge } from '../common/Badge' -import { Button } from '../common/Button' -import { Toolbar } from '../common/Toolbar' -import { UpsellDialog } from '../common/UpsellDialog' +import { WithFilteredSnippetsContext } from '../../../hooks/useFilteredSnippets' +import { WithRestAPIContext } from '../../../hooks/useRestAPI' +import { WithSnippetsListContext } from '../../../hooks/useSnippetsList' +import { WithSnippetsTableFiltersContext, useSnippetsFilters } from '../../../hooks/useSnippetsFilters' +import { SNIPPET_TYPES } from '../../../types/Snippet' +import { isLicensed } from '../../../utils/screen' +import { SNIPPET_TYPE_LABELS, getSnippetEditUrl, isProType } from '../../../utils/snippets/snippets' +import { Badge } from '../../common/Badge' +import { Button } from '../../common/Button' +import { UpsellDialog } from '../../common/UpsellDialog' import { SnippetsListTable } from './SnippetsListTable' -import type { SnippetType } from '../../types/Snippet' +import type { SnippetType } from '../../../types/Snippet' interface SnippetTypeTabProps { type?: SnippetType @@ -54,7 +53,7 @@ const SnippetTypeTab: React.FC = ({ type, setIsUpgradeDialo const PageHeading = () => { const { searchQueryText, searchLineNumber, currentTag, setSearchQuery, setCurrentTag } = useSnippetsFilters() return ( -
+

{__('Manage Code Snippets', 'code-snippets')}

{searchQueryText || currentTag @@ -113,7 +112,6 @@ export const SnippetsTable: React.FC = () => - diff --git a/src/js/components/SnippetsTable/TableColumns.tsx b/src/js/components/ManageMenu/SnippetsTable/TableColumns.tsx similarity index 89% rename from src/js/components/SnippetsTable/TableColumns.tsx rename to src/js/components/ManageMenu/SnippetsTable/TableColumns.tsx index 55b7649a..2b9d6c97 100644 --- a/src/js/components/SnippetsTable/TableColumns.tsx +++ b/src/js/components/ManageMenu/SnippetsTable/TableColumns.tsx @@ -3,19 +3,19 @@ import { __, sprintf } from '@wordpress/i18n' import { addQueryArgs } from '@wordpress/url' import { humanTimeDiff } from '@wordpress/date' import { RawHTML } from '@wordpress/element' -import { useFilteredSnippets } from '../../hooks/useFilteredSnippets' -import { useRestAPI } from '../../hooks/useRestAPI' -import { useSnippetsFilters } from '../../hooks/useSnippetsFilters' -import { useSnippetsList } from '../../hooks/useSnippetsList' -import { handleUnknownError } from '../../utils/errors' -import { downloadSnippetExportFile } from '../../utils/files' -import { isNetworkAdmin } from '../../utils/screen' -import { getSnippetDisplayName, getSnippetEditUrl, getSnippetType } from '../../utils/snippets/snippets' -import { Badge } from '../common/Badge' -import { Button } from '../common/Button' -import { DeleteButton } from '../common/DeleteButton' -import type { Snippet } from '../../types/Snippet' -import type { ListTableColumn } from '../common/ListTable' +import { useFilteredSnippets } from '../../../hooks/useFilteredSnippets' +import { useRestAPI } from '../../../hooks/useRestAPI' +import { useSnippetsFilters } from '../../../hooks/useSnippetsFilters' +import { useSnippetsList } from '../../../hooks/useSnippetsList' +import { handleUnknownError } from '../../../utils/errors' +import { downloadSnippetExportFile } from '../../../utils/files' +import { isNetworkAdmin } from '../../../utils/screen' +import { getSnippetDisplayName, getSnippetEditUrl, getSnippetType } from '../../../utils/snippets/snippets' +import { Badge } from '../../common/Badge' +import { Button } from '../../common/Button' +import { DeleteButton } from '../../common/DeleteButton' +import type { Snippet } from '../../../types/Snippet' +import type { ListTableColumn } from '../../common/ListTable' interface ColumnProps { snippet: Snippet diff --git a/src/js/components/SnippetsTable/index.ts b/src/js/components/ManageMenu/SnippetsTable/index.ts similarity index 100% rename from src/js/components/SnippetsTable/index.ts rename to src/js/components/ManageMenu/SnippetsTable/index.ts diff --git a/src/js/components/ManageMenu/index.ts b/src/js/components/ManageMenu/index.ts new file mode 100644 index 00000000..c60601a7 --- /dev/null +++ b/src/js/components/ManageMenu/index.ts @@ -0,0 +1 @@ +export * from './ManageMenu' diff --git a/src/js/components/WelcomePage/Changelog.tsx b/src/js/components/WelcomeMenu/Changelog.tsx similarity index 100% rename from src/js/components/WelcomePage/Changelog.tsx rename to src/js/components/WelcomeMenu/Changelog.tsx diff --git a/src/js/components/WelcomePage/WelcomePage.tsx b/src/js/components/WelcomeMenu/WelcomeMenu.tsx similarity index 98% rename from src/js/components/WelcomePage/WelcomePage.tsx rename to src/js/components/WelcomeMenu/WelcomeMenu.tsx index 6ef33fba..145a03d2 100644 --- a/src/js/components/WelcomePage/WelcomePage.tsx +++ b/src/js/components/WelcomeMenu/WelcomeMenu.tsx @@ -82,7 +82,7 @@ const Articles: React.FC = ({ articles }) => -export const WelcomePage = () => +export const WelcomeMenu = () => <>
diff --git a/src/js/components/WelcomeMenu/index.ts b/src/js/components/WelcomeMenu/index.ts new file mode 100644 index 00000000..c6aa4faa --- /dev/null +++ b/src/js/components/WelcomeMenu/index.ts @@ -0,0 +1 @@ +export * from './WelcomeMenu' diff --git a/src/js/components/WelcomePage/index.ts b/src/js/components/WelcomePage/index.ts deleted file mode 100644 index ef5b08c9..00000000 --- a/src/js/components/WelcomePage/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './WelcomePage' diff --git a/src/js/components/common/DismissableNotice.tsx b/src/js/components/common/DismissableNotice.tsx index fd661dd1..abac3fc2 100644 --- a/src/js/components/common/DismissableNotice.tsx +++ b/src/js/components/common/DismissableNotice.tsx @@ -10,10 +10,10 @@ export interface DismissibleNoticeProps { } export const DismissibleNotice: React.FC = ({ className, onDismiss, children }) => -
+
<>{children} -
-const currentPage = fetchQueryParam('page') +const currentPage = fetchQueryParam('subpage') ?? fetchQueryParam('page') const LowerNav: React.FC = ({ setIsUpsellDialogOpen }) =>
diff --git a/src/js/entries/edit.ts b/src/js/entries/edit.ts index ca3dc6ca..c5c4676e 100644 --- a/src/js/entries/edit.ts +++ b/src/js/entries/edit.ts @@ -1,4 +1,4 @@ -import { SnippetForm } from '../components/SnippetForm' +import { EditMenu } from '../components/EditMenu' import { loadComponent } from '../utils/bootstrap' -loadComponent('edit-snippet-form-container', SnippetForm) +loadComponent('edit-snippet-container', EditMenu) diff --git a/src/js/entries/manage.ts b/src/js/entries/manage.ts index 5dd1e8d3..55bf4b8d 100644 --- a/src/js/entries/manage.ts +++ b/src/js/entries/manage.ts @@ -1,4 +1,4 @@ -import { SnippetsTable } from '../components/SnippetsTable' +import { ManageMenu } from '../components/ManageMenu' import { loadComponent } from '../utils/bootstrap' -loadComponent('snippets-table-container', SnippetsTable) +loadComponent('manage-snippets-container', ManageMenu) diff --git a/src/js/entries/welcome.ts b/src/js/entries/welcome.ts index f7686591..b552a194 100644 --- a/src/js/entries/welcome.ts +++ b/src/js/entries/welcome.ts @@ -1,4 +1,4 @@ -import { WelcomePage } from '../components/WelcomePage' +import { WelcomeMenu } from '../components/WelcomeMenu' import { loadComponent } from '../utils/bootstrap' -loadComponent('code-snippets-welcome-container', WelcomePage) +loadComponent('code-snippets-welcome-container', WelcomeMenu) diff --git a/src/js/hooks/useCloudSearch.tsx b/src/js/hooks/useCloudSearch.tsx new file mode 100644 index 00000000..b6a78eae --- /dev/null +++ b/src/js/hooks/useCloudSearch.tsx @@ -0,0 +1,72 @@ +import React, { Dispatch, PropsWithChildren, SetStateAction, useEffect, useState } from 'react' +import type { CloudSnippetSchema } from '../types/schema/CloudSnippetSchema' +import { createContextHook } from '../utils/bootstrap' +import { REST_CLOUD_SEARCH_BASE } from '../utils/restAPI' +import { useRestAPI } from './useRestAPI' +import { addQueryArgs } from '@wordpress/url' + +interface CloudSearchContext { + page: number + error: boolean + query: string + totalItems: number + totalPages: number + isSearching: boolean + searchResults: CloudSnippetSchema[] | undefined + setPage: Dispatch> + setQuery: Dispatch> + searchByCodevault: boolean + setSearchByCodevault: Dispatch> +} + +export const [CloudSearchContext, useCloudSearch] = createContextHook('useCloudSearch') + +export const WithCloudSearchContext: React.FC = ({ children }) => { + const { api } = useRestAPI() + const [page, setPage] = useState(0) + const [query, setQuery] = useState('') + const [searchByCodevault, setSearchByCodevault] = useState(false) + + const [totalItems, setTotalItems] = useState(0) + const [totalPages, setTotalPages] = useState(0) + + const [searchResults, setSearchResults] = useState() + const [isSearching, setIsSearching] = useState(false) + const [error, setError] = useState(false) + + useEffect(() => { + if (0 < page) { + setIsSearching(true) + + api + .getResponse(addQueryArgs(REST_CLOUD_SEARCH_BASE, { query, searchByCodevault, page })) + .then(response => { + setTotalItems(Number(response.headers['x-wp-total'])) + setTotalPages(Number(response.headers['x-wp-totalpages'])) + setSearchResults(response.data) + setIsSearching(false) + }) + .catch((error: unknown) => { + console.error(error) + setIsSearching(false) + setError(true) + }) + } + }, [api, page, query, searchByCodevault, setError, setSearchResults, setTotalItems, setTotalPages]) + + const value: CloudSearchContext = { + page, + error, + query, + setPage, + setQuery, + totalItems, + totalPages, + isSearching, + searchResults, + searchByCodevault, + setSearchByCodevault + } + + return {children} +} diff --git a/src/js/hooks/useRestAPI.tsx b/src/js/hooks/useRestAPI.tsx index 09b3b847..7f703ab6 100644 --- a/src/js/hooks/useRestAPI.tsx +++ b/src/js/hooks/useRestAPI.tsx @@ -15,6 +15,7 @@ export interface RestAPIContext { export interface RestAPI { get: (url: string) => Promise + getResponse: (url: string) => Promise> post: (url: string, data?: object) => Promise put: (url: string, data?: object) => Promise del: (url: string) => Promise @@ -25,29 +26,36 @@ const debugRequest = async ( url: string, doRequest: Promise>, data?: D -): Promise => { +): Promise> => { if (window.CODE_SNIPPETS?.debug) { console.debug(`${method} ${url}`, ...data ? [data] : []) const response = await doRequest console.debug('Response', response) - return response.data + return response } else { - return (await doRequest).data + return await doRequest } } const buildRestAPI = (axiosInstance: AxiosInstance): RestAPI => ({ - get: (url: string): Promise => + getResponse: (url: string): Promise> => debugRequest('GET', url, axiosInstance.get, never>(url)), + get: (url: string): Promise => + debugRequest('GET', url, axiosInstance.get, never>(url)) + .then(response => response.data), + post: (url: string, data?: object): Promise => - debugRequest('POST', url, axiosInstance.post>(url, data), data), + debugRequest('POST', url, axiosInstance.post>(url, data), data) + .then(response => response.data), del: (url: string): Promise => - debugRequest('DELETE', url, axiosInstance.delete, never>(url)), + debugRequest('DELETE', url, axiosInstance.delete, never>(url)) + .then(response => response.data), put: (url: string, data?: object): Promise => debugRequest('PUT', url, axiosInstance.put>(url, data), data) + .then(response => response.data), }) export const [RestAPIContext, useRestAPI] = createContextHook('useRestAPI') diff --git a/src/js/services/manage/requests.ts b/src/js/services/manage/requests.ts index fe23393c..b8dd38ff 100644 --- a/src/js/services/manage/requests.ts +++ b/src/js/services/manage/requests.ts @@ -42,11 +42,14 @@ export const updateSnippet = (field: keyof Snippet, row: Element, snippet: Parti return } - snippet.id = parseInt(columnId.textContent, 10) - snippet.shared_network = null !== /\bshared-network-snippet\b/.exec(row.className) - snippet.network = snippet.shared_network || isNetworkAdmin() - snippet.scope = row.getAttribute('data-snippet-scope') ?? snippet.scope + const updatedSnippet: Partial = { + ...snippet, + id: parseInt(columnId.textContent, 10), + shared_network: null !== /\bshared-network-snippet\b/.exec(row.className), + network: snippet.shared_network ?? isNetworkAdmin(), + scope: ( row.getAttribute('data-snippet-scope')) ?? snippet.scope + } - const queryString = `action=update_code_snippet&_ajax_nonce=${nonce.value}&field=${field}&snippet=${JSON.stringify(snippet)}` + const queryString = `action=update_code_snippet&_ajax_nonce=${nonce.value}&field=${field}&snippet=${JSON.stringify(updatedSnippet)}` sendSnippetRequest(queryString, successCallback) } diff --git a/src/js/types/Window.ts b/src/js/types/Window.ts index 85ec3dce..1075aed6 100644 --- a/src/js/types/Window.ts +++ b/src/js/types/Window.ts @@ -26,6 +26,7 @@ declare global { base: string snippets: string conditions: string + cloudSearch: string cloud: string nonce: string localToken: string diff --git a/src/js/types/schema/CloudSnippetSchema.ts b/src/js/types/schema/CloudSnippetSchema.ts new file mode 100644 index 00000000..afd65faa --- /dev/null +++ b/src/js/types/schema/CloudSnippetSchema.ts @@ -0,0 +1,29 @@ +import type { SnippetScope } from '../Snippet' + +export interface CloudSnippetSchema { + id: number + cloud_id?: string + name: string + description: string + code: string + tags: string[] + scope: SnippetScope + status: CloudStatus + codevault: string + total_votes: number + vote_count: number + wp_tested: string + created: string + updated: string + revision: number + is_owner: boolean + shared_network: boolean +} + +export enum CloudStatus { + Private = 3, + Public = 4, + Unverified = 5, + AI_Verified = 6, + Pro_Verified = 8 +} diff --git a/src/js/utils/Prism.ts b/src/js/utils/Prism.ts new file mode 100644 index 00000000..1acf0b30 --- /dev/null +++ b/src/js/utils/Prism.ts @@ -0,0 +1,17 @@ +import Prism from 'prismjs' +import 'prismjs/components/prism-markup' +import 'prismjs/components/prism-markup-templating' +import 'prismjs/components/prism-clike' +import 'prismjs/components/prism-css' +import 'prismjs/components/prism-php' +import 'prismjs/components/prism-javascript' +import 'prismjs/plugins/line-highlight/prism-line-highlight' +import 'prismjs/plugins/line-numbers/prism-line-numbers' +import 'prismjs/plugins/toolbar/prism-toolbar' +import 'prismjs/plugins/show-language/prism-show-language' +import 'prismjs/plugins/copy-to-clipboard/prism-copy-to-clipboard' +import 'prismjs/plugins/inline-color/prism-inline-color' +import 'prismjs/plugins/previewers/prism-previewers' +import 'prismjs/plugins/autolinker/prism-autolinker' + +export { Prism } diff --git a/src/js/utils/bootstrap.tsx b/src/js/utils/bootstrap.tsx index b83fd533..29744ae5 100644 --- a/src/js/utils/bootstrap.tsx +++ b/src/js/utils/bootstrap.tsx @@ -4,7 +4,6 @@ import type { Context, FunctionComponent } from 'react' export const loadComponent = (containerId: string, Component: FunctionComponent): void => { const container = document.getElementById(containerId) - console.log('loading on', container) if (container) { const root = createRoot(container) diff --git a/src/js/utils/restAPI.ts b/src/js/utils/restAPI.ts index f2888bc6..3a903a7a 100644 --- a/src/js/utils/restAPI.ts +++ b/src/js/utils/restAPI.ts @@ -5,6 +5,7 @@ export const REST_API_NAMESPACE = 'code-snippets' export const REST_BASE = trimTrailingChar(window.CODE_SNIPPETS?.restAPI.base ?? '', '/') export const REST_SNIPPETS_BASE = trimTrailingChar(window.CODE_SNIPPETS?.restAPI.snippets ?? '', '/') +export const REST_CLOUD_SEARCH_BASE = trimTrailingChar(window.CODE_SNIPPETS?.restAPI.cloudSearch ?? '', '/') export const REST_API_AXIOS_CONFIG: AxiosRequestConfig = { headers: { diff --git a/src/php/admin-menus/class-edit-menu.php b/src/php/admin-menus/class-edit-menu.php index a8d63874..44c6922f 100644 --- a/src/php/admin-menus/class-edit-menu.php +++ b/src/php/admin-menus/class-edit-menu.php @@ -110,7 +110,7 @@ protected function ensure_correct_page() { * @return void */ public function render() { - echo '
'; + echo '
'; } /** diff --git a/src/php/admin-menus/class-manage-menu.php b/src/php/admin-menus/class-manage-menu.php index b18e9fa6..e5dcf911 100644 --- a/src/php/admin-menus/class-manage-menu.php +++ b/src/php/admin-menus/class-manage-menu.php @@ -244,7 +244,7 @@ protected function get_snippets_per_page(): int { * @return void */ public function render() { - echo '
'; + echo '
'; } /** diff --git a/src/php/class-admin.php b/src/php/class-admin.php index 7fc7e199..f95b2ef9 100644 --- a/src/php/class-admin.php +++ b/src/php/class-admin.php @@ -283,7 +283,7 @@ public function print_notices() { } printf( - '

', + '