diff --git a/.github/workflows/eslint.yml b/.github/workflows/eslint.yml index 80aec41..e7bbbc4 100644 --- a/.github/workflows/eslint.yml +++ b/.github/workflows/eslint.yml @@ -30,9 +30,6 @@ jobs: - name: Install Node dependencies run: npm ci --omit=optional - - name: Install eslint-plugin-prettier v5.0.0 # Do this to get around a bug in the version that @10up/eslint-config requires. - run: npm install eslint-plugin-prettier@^5.0.0 - - name: Get updated JS files id: changed-files uses: tj-actions/changed-files@v41 @@ -42,5 +39,4 @@ jobs: - name: Run JS linting if: ${{ steps.changed-files.outputs.any_changed == 'true' }} - run: | - npx 10up-toolkit lint-js ${{ steps.changed-files.outputs.all_changed_files }} + run: ./node_modules/.bin/10up-toolkit lint-js ${{ steps.changed-files.outputs.all_changed_files }} diff --git a/assets/js/admin/indexer.js b/assets/js/admin/indexer.js index 22f288e..71e9d27 100644 --- a/assets/js/admin/indexer.js +++ b/assets/js/admin/indexer.js @@ -112,7 +112,10 @@ class Indexer extends EventTarget { cancel() { this.cancelPending(); - this.triggerEvent('indexCancel', { progress: this.progress, total: this.total }); + this.triggerEvent('indexCancel', { + progress: this.progress, + total: this.total, + }); } loadTerms(opts) { @@ -150,69 +153,11 @@ class Indexer extends EventTarget { }); } - async deleteIndex(ids, opts) { + async deleteIndex(opts) { this.progress = 0; this.completed = 0; this.failures = 0; - this.total = ids.length; - this.triggerEvent('deleteIndexStart', { progress: 0, total: this.total }); - - const chunks = this.toChunks(ids, opts.batchSize || 50); - const n = chunks.length; - - for (let i = 0; i < n; i++) { - const batch = chunks[i]; - try { - await this.deleteIndexBatch(batch, opts); // eslint-disable-line no-await-in-loop - } catch (e) { - this.failures += batch.length; - this.triggerEvent('deleteIndexError', e); - } - } - - this.triggerEvent('deleteIndexComplete', { - progress: this.progress, - total: this.total, - completed: this.completed, - failures: this.failures, - }); - } - - async deleteIndexBatch(batch, opts = {}) { - const fetchOpts = { - url: opts.endpoint, - method: 'POST', - data: { - term_ids: batch, - }, - ...opts, - }; - - const promise = this.apiFetch(fetchOpts); - - promise.then((res) => { - if (res.errors) { - this.failures += batch.length; - this.triggerEvent('deleteIndexError', res); - } else if (!res.success && res.data) { - this.failures += batch.length; - this.triggerEvent('deleteIndexError', res); - } else { - this.completed += batch.length; - } - - this.progress += batch.length; - this.triggerEvent('deleteIndexProgress', { - progress: this.progress, - total: this.total, - ...res, - }); - }); - - return promise; - } - - deleteIndexBulk(opts) { + this.total = 0; this.triggerEvent('deleteIndexStart'); const fetchOpts = { diff --git a/assets/js/admin/tools.js b/assets/js/admin/tools.js index f3f79a2..f3577fc 100755 --- a/assets/js/admin/tools.js +++ b/assets/js/admin/tools.js @@ -22,10 +22,6 @@ class ToolsApp { this.onIndex('indexCancel', 'didIndexCancel'); this.onIndex('indexError', 'didIndexError'); - this.onIndex('loadTermsStart', 'didLoadTermsStart'); - this.onIndex('loadTermsComplete', 'didLoadTermsComplete'); - this.onIndex('loadTermsError', 'didLoadTermsError'); - this.onIndex('deleteIndexStart', 'didDeleteIndexStart'); this.onIndex('deleteIndexProgress', 'didDeleteIndexProgress'); this.onIndex('deleteIndexComplete', 'didDeleteIndexComplete'); @@ -247,57 +243,6 @@ class ToolsApp { this.updateProgress(); } - didLoadTermsStart() { - const message = __('Loading block catalog terms to delete ...', 'block-catalog'); - - this.setState({ status: 'loading-terms', message }); - this.hideErrors(); - this.setNotice(''); - - window.scrollTo(0, 0); - } - - didLoadTermsComplete(event) { - const message = __('Loaded terms to delete, starting ...', 'block-catalog'); - this.setState({ status: 'loaded-terms', message, ...event.detail }); - - const opts = { - batchSize: this.settings?.delete_index_batch_size, - endpoint: this.settings?.delete_index_endpoint, - }; - - let { terms } = this.state; - terms = terms.map((term) => term.id); - - this.indexer.deleteIndex(terms, opts); - } - - didLoadTermsError(event) { - const err = event.detail || {}; - - let message = __('Failed to load terms to delete.', 'block-catalog'); - - if (err?.terms?.length === 0) { - message = __('Block catalog is empty, nothing to delete.', 'block-catalog'); - this.setState({ status: 'load-terms-error', message: '', error: '' }); - this.setNotice(message, 'error'); - return; - } - - if (err?.message) { - message += ` (${err?.code} - ${err.message})`; - } - - if (err?.data?.message) { - message += ` (${err.data.message})`; - } else if (typeof err?.data === 'string') { - message += ` (${err.data})`; - } - - this.setState({ status: 'load-terms-error', message: '', error: err }); - this.setNotice(message, 'error'); - } - didDeleteIndexStart(event) { const message = __('Deleting Index ...', 'block-catalog'); this.setState({ status: 'deleting', message, ...event.detail }); @@ -324,10 +269,10 @@ class ToolsApp { ); this.setNotice(message, 'error'); - } else if (event.detail?.completed) { + } else if (event.detail?.removed) { message = sprintf( __('Deleted %d block catalog term(s) successfully.', 'block-catalog'), - event.detail?.completed, + event.detail?.removed, ); this.setNotice(message, 'success'); @@ -390,10 +335,10 @@ class ToolsApp { } const opts = { - endpoint: this.settings.terms_endpoint, + endpoint: this.settings.delete_index_endpoint, }; - this.indexer.loadTerms(opts); + this.indexer.deleteIndex(opts); return false; } diff --git a/includes/classes/CatalogBuilder.php b/includes/classes/CatalogBuilder.php index a8fbbde..048c2fd 100644 --- a/includes/classes/CatalogBuilder.php +++ b/includes/classes/CatalogBuilder.php @@ -42,51 +42,77 @@ public function catalog( $post_id, $opts = [] ) { } /** - * Resets the Block Catalog by removing all catalog terms. + * Bulk deletes all block catalog terms and their relationships via Direct DB query. + * This is a faster alternative to wp_delete_term() which is slow for large catalogs. * * @param array $opts Optional args + * @return array */ public function delete_index( $opts = [] ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found - \BlockCatalog\Utility\start_bulk_operation(); - - $term_opts = [ - 'taxonomy' => BLOCK_CATALOG_TAXONOMY, - 'fields' => 'ids', - 'hide_empty' => false, - ]; + global $wpdb; - $terms = get_terms( $term_opts ); - $total = count( $terms ); - $removed = 0; $errors = 0; - $is_cli = defined( 'WP_CLI' ) && WP_CLI; - - // translators: %d is number of block catalog terms - $message = sprintf( __( 'Removing %d block catalog terms ...', 'block-catalog' ), $total ); + $removed = 0; - if ( $is_cli ) { - $progress_bar = \WP_CLI\Utils\make_progress_bar( $message, $total ); + // Delete term relationships + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery + $term_relationships = $wpdb->query( + $wpdb->prepare( + "DELETE FROM {$wpdb->term_relationships} + WHERE term_taxonomy_id IN ( + SELECT term_taxonomy_id FROM {$wpdb->term_taxonomy} WHERE taxonomy = %s + )", + BLOCK_CATALOG_TAXONOMY + ) + ); + + if ( false === $term_relationships ) { + ++$errors; } - foreach ( $terms as $term_id ) { - if ( $is_cli ) { - $progress_bar->tick(); - } + // Delete term taxonomy + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery + $term_taxonomy = $wpdb->query( + $wpdb->prepare( + "DELETE FROM {$wpdb->term_taxonomy} WHERE taxonomy = %s", + BLOCK_CATALOG_TAXONOMY + ) + ); + + if ( false === $term_taxonomy ) { + ++$errors; + } else { + $removed = $term_taxonomy; + } - $result = $this->delete_term_index( $term_id ); + // Delete terms + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery + $terms = $wpdb->query( + $wpdb->prepare( + "DELETE FROM {$wpdb->terms} + WHERE term_id IN ( + SELECT term_id FROM {$wpdb->term_taxonomy} WHERE taxonomy = %s + )", + BLOCK_CATALOG_TAXONOMY + ) + ); + + if ( false === $terms ) { + ++$errors; + } - \BlockCatalog\Utility\clear_caches(); + // update block catalog term counts = 0 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery + $wpdb->query( + $wpdb->prepare( + "UPDATE {$wpdb->term_taxonomy} SET count = 0 WHERE taxonomy = %s", + BLOCK_CATALOG_TAXONOMY + ) + ); - if ( ! is_wp_error( $result ) ) { - ++$removed; - } else { - ++$errors; - } - } + clean_term_cache( [], BLOCK_CATALOG_TAXONOMY, true ); - if ( $is_cli ) { - $progress_bar->finish(); - } + $is_cli = defined( 'WP_CLI' ) && WP_CLI; if ( $is_cli ) { if ( ! empty( $removed ) ) { @@ -97,12 +123,11 @@ public function delete_index( $opts = [] ) { // phpcs:ignore Generic.CodeAnalysi } if ( ! empty( $errors ) ) { + // translators: %d is number of catalog terms removed \WP_CLI::warning( sprintf( 'Failed to remove %d block catalog terms(s).', 'block-catalog' ), $errors ); } } - \BlockCatalog\Utility\stop_bulk_operation(); - return [ 'removed' => $removed, 'errors' => $errors, diff --git a/includes/classes/RESTSupport.php b/includes/classes/RESTSupport.php index e671b17..de9e6ab 100644 --- a/includes/classes/RESTSupport.php +++ b/includes/classes/RESTSupport.php @@ -91,13 +91,6 @@ public function register_endpoints() { 'permission_callback' => function () { return current_user_can( \BlockCatalog\Utility\get_required_capability() ); }, - 'args' => [ - 'term_ids' => [ - 'required' => true, - 'type' => 'array', - 'validate_callback' => [ $this, 'validate_term_ids' ], - ], - ], ] ); } @@ -136,28 +129,18 @@ public function get_terms() { /** * Deletes the Block catalog index. * - * @param \WP_REST_Request $request The request object * @return array */ - public function delete_index( $request ) { + public function delete_index() { \BlockCatalog\Utility\start_bulk_operation(); - $term_ids = $request->get_param( 'term_ids' ); - $updated = 0; - $errors = 0; - $builder = new CatalogBuilder(); - - foreach ( $term_ids as $term_id ) { - $result = $builder->delete_term_index( $term_id ); + $updated = 0; + $errors = 0; + $builder = new CatalogBuilder(); - \BlockCatalog\Utility\clear_caches(); - - if ( is_wp_error( $result ) ) { - ++$errors; - } else { - ++$updated; - } - } + $result = $builder->delete_index( [ 'bulk' => true ] ); + $updated = $result['removed']; + $errors = $result['errors']; \BlockCatalog\Utility\stop_bulk_operation(); @@ -318,21 +301,4 @@ public function validate_post_ids( $post_ids ) { return ! empty( $post_ids ); } - - /** - * Validates the specified term ids. - * - * @param array $term_ids The term ids to validate - * @return bool - */ - public function validate_term_ids( $term_ids ) { - if ( empty( $term_ids ) ) { - return true; - } - - $term_ids = array_map( 'intval', $term_ids ); - $term_ids = array_filter( $term_ids ); - - return ! empty( $term_ids ); - } } diff --git a/tests/classes/CatalogBuilderTest.php b/tests/classes/CatalogBuilderTest.php index 838e850..729fd11 100644 --- a/tests/classes/CatalogBuilderTest.php +++ b/tests/classes/CatalogBuilderTest.php @@ -403,9 +403,10 @@ function test_it_can_delete_block_catalog_index() { $this->builder->catalog( $post_id ); } - $this->builder->delete_index(); + $result = $this->builder->delete_index(); $actual = get_terms( [ 'taxonomy' => BLOCK_CATALOG_TAXONOMY ] ); $this->assertEmpty( $actual ); } + }