diff --git a/.wp-env.json b/.wp-env.json index 61758738..23d552f6 100644 --- a/.wp-env.json +++ b/.wp-env.json @@ -6,6 +6,7 @@ ], "themes": [], "mappings": { + "wp-content/mu-plugins/visualizer-e2e-enable-database-source.php": "./tests/e2e/config/enable-database-source.php", "wp-content/mu-plugins/visualizer-e2e-force-lazy-render.php": "./tests/e2e/config/force-lazy-render.php" }, "config": { diff --git a/composer.lock b/composer.lock index 106a4329..df2b7b77 100644 --- a/composer.lock +++ b/composer.lock @@ -8,16 +8,16 @@ "packages": [ { "name": "codeinwp/themeisle-sdk", - "version": "3.3.55", + "version": "3.3.57", "source": { "type": "git", "url": "https://github.com/Codeinwp/themeisle-sdk.git", - "reference": "bd601798d209a4bc5962d2a19a22dc6dddf341cc" + "reference": "3c761b0bddda8d5963a47d14a40811869131030b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeinwp/themeisle-sdk/zipball/bd601798d209a4bc5962d2a19a22dc6dddf341cc", - "reference": "bd601798d209a4bc5962d2a19a22dc6dddf341cc", + "url": "https://api.github.com/repos/Codeinwp/themeisle-sdk/zipball/3c761b0bddda8d5963a47d14a40811869131030b", + "reference": "3c761b0bddda8d5963a47d14a40811869131030b", "shasum": "" }, "require-dev": { @@ -43,9 +43,9 @@ ], "support": { "issues": "https://github.com/Codeinwp/themeisle-sdk/issues", - "source": "https://github.com/Codeinwp/themeisle-sdk/tree/v3.3.55" + "source": "https://github.com/Codeinwp/themeisle-sdk/tree/v3.3.57" }, - "time": "2026-07-20T10:57:27+00:00" + "time": "2026-07-23T13:31:25+00:00" }, { "name": "neitanod/forceutf8", @@ -224,16 +224,16 @@ "packages-dev": [ { "name": "dealerdirect/phpcodesniffer-composer-installer", - "version": "v1.2.0", + "version": "v1.2.1", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/composer-installer.git", - "reference": "845eb62303d2ca9b289ef216356568ccc075ffd1" + "reference": "963f0c67bffde0eac41b56be71ac0e8ba132f0bd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/845eb62303d2ca9b289ef216356568ccc075ffd1", - "reference": "845eb62303d2ca9b289ef216356568ccc075ffd1", + "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/963f0c67bffde0eac41b56be71ac0e8ba132f0bd", + "reference": "963f0c67bffde0eac41b56be71ac0e8ba132f0bd", "shasum": "" }, "require": { @@ -316,7 +316,7 @@ "type": "thanks_dev" } ], - "time": "2025-11-11T04:32:07+00:00" + "time": "2026-05-06T08:26:05+00:00" }, { "name": "doctrine/instantiator", @@ -891,21 +891,21 @@ }, { "name": "phpcsstandards/phpcsextra", - "version": "1.5.0", + "version": "1.5.1", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHPCSExtra.git", - "reference": "b598aa890815b8df16363271b659d73280129101" + "reference": "39467533fdb742446d68c1d10ac33d625ee0311c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/b598aa890815b8df16363271b659d73280129101", - "reference": "b598aa890815b8df16363271b659d73280129101", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/39467533fdb742446d68c1d10ac33d625ee0311c", + "reference": "39467533fdb742446d68c1d10ac33d625ee0311c", "shasum": "" }, "require": { "php": ">=5.4", - "phpcsstandards/phpcsutils": "^1.2.0", + "phpcsstandards/phpcsutils": "^1.2.3", "squizlabs/php_codesniffer": "^3.13.5 || ^4.0.1" }, "require-dev": { @@ -969,20 +969,20 @@ "type": "thanks_dev" } ], - "time": "2025-11-12T23:06:57+00:00" + "time": "2026-07-27T11:13:17+00:00" }, { "name": "phpcsstandards/phpcsutils", - "version": "1.2.2", + "version": "1.2.3", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHPCSUtils.git", - "reference": "c216317e96c8b3f5932808f9b0f1f7a14e3bbf55" + "reference": "5f35d9408c54d7b529501f3c688b6eae562aea1f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/c216317e96c8b3f5932808f9b0f1f7a14e3bbf55", - "reference": "c216317e96c8b3f5932808f9b0f1f7a14e3bbf55", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/5f35d9408c54d7b529501f3c688b6eae562aea1f", + "reference": "5f35d9408c54d7b529501f3c688b6eae562aea1f", "shasum": "" }, "require": { @@ -1062,15 +1062,15 @@ "type": "thanks_dev" } ], - "time": "2025-12-08T14:27:58+00:00" + "time": "2026-07-27T10:28:41+00:00" }, { "name": "phpstan/phpstan", - "version": "2.1.54", + "version": "2.2.6", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/8be50c3992107dc837b17da4d140fbbdf9a5c5bd", - "reference": "8be50c3992107dc837b17da4d140fbbdf9a5c5bd", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/a6e9b5a9420f6109c091e87d82683bd1a80b87ed", + "reference": "a6e9b5a9420f6109c091e87d82683bd1a80b87ed", "shasum": "" }, "require": { @@ -1093,6 +1093,17 @@ "license": [ "MIT" ], + "authors": [ + { + "name": "Ondřej Mirtes" + }, + { + "name": "Markus Staab" + }, + { + "name": "Vincent Langlet" + } + ], "description": "PHPStan - PHP Static Analysis Tool", "keywords": [ "dev", @@ -1115,7 +1126,7 @@ "type": "github" } ], - "time": "2026-04-29T13:31:09+00:00" + "time": "2026-07-26T21:22:49+00:00" }, { "name": "phpunit/php-code-coverage", @@ -2752,16 +2763,16 @@ }, { "name": "wp-coding-standards/wpcs", - "version": "3.3.0", + "version": "3.4.1", "source": { "type": "git", "url": "https://github.com/WordPress/WordPress-Coding-Standards.git", - "reference": "7795ec6fa05663d716a549d0b44e47ffc8b0d4a6" + "reference": "ec2ff942335f33683a5957a85d138753876a05cf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/7795ec6fa05663d716a549d0b44e47ffc8b0d4a6", - "reference": "7795ec6fa05663d716a549d0b44e47ffc8b0d4a6", + "url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/ec2ff942335f33683a5957a85d138753876a05cf", + "reference": "ec2ff942335f33683a5957a85d138753876a05cf", "shasum": "" }, "require": { @@ -2770,9 +2781,9 @@ "ext-tokenizer": "*", "ext-xmlreader": "*", "php": ">=7.2", - "phpcsstandards/phpcsextra": "^1.5.0", - "phpcsstandards/phpcsutils": "^1.1.0", - "squizlabs/php_codesniffer": "^3.13.4" + "phpcsstandards/phpcsextra": "^1.5.1", + "phpcsstandards/phpcsutils": "^1.2.3", + "squizlabs/php_codesniffer": "^3.13.5" }, "require-dev": { "php-parallel-lint/php-console-highlighter": "^1.0.0", @@ -2814,7 +2825,7 @@ "type": "custom" } ], - "time": "2025-11-25T12:08:04+00:00" + "time": "2026-07-27T11:53:23+00:00" }, { "name": "yoast/phpunit-polyfills", diff --git a/js/frame.js b/js/frame.js index e8596dca..6fd62e26 100644 --- a/js/frame.js +++ b/js/frame.js @@ -479,6 +479,34 @@ init_db_import_component(); + var settings_button = document.querySelector( '#settings-button' ); + if ( settings_button ) { + settings_button.addEventListener( 'click', function( event ){ + $('body').trigger('visualizer:db:query:update', {}); + if( $( '#db-chart-button' ).attr( 'data-current' ) !== 'filter' || $( '.visualizer-db-query' ).val().length === 0 ){ + return; + } + + event.preventDefault(); + event.stopImmediatePropagation(); + + var query_saved = false; + var resume_save = function(){ + query_saved = true; + settings_button.click(); + }; + + $('body').one( 'visualizer:render:currentchart:update', resume_save ); + $( '#thehole' ).one( 'load', function(){ + $('body').off( 'visualizer:render:currentchart:update', resume_save ); + if ( ! query_saved ) { + $( '#db-chart-button' ).trigger( 'click' ); + } + } ); + $( '#db-chart-button' ).trigger( 'click' ); + }, true ); + } + $('#visualizer-query-fetch').on('click', function(e){ $('body').trigger('visualizer:db:query:update', {}); diff --git a/tests/e2e/config/enable-database-source.php b/tests/e2e/config/enable-database-source.php new file mode 100644 index 00000000..ecc22bb7 --- /dev/null +++ b/tests/e2e/config/enable-database-source.php @@ -0,0 +1,26 @@ + button.click() ); + await expect( chartEditor.locator( '#visualizer-db-query' ) ).toBeVisible(); +} + +async function expectChartCell( page, value ) { + // Chart scripts lazy-load on user interaction and Google Charts can miss + // the first trigger, so keep re-scrolling until the table renders. + await expect( async () => { + await page.mouse.wheel( 0, 100 ); + await expect( page.getByRole( 'cell', { name: value } ) ).toBeVisible( { + timeout: 5000, + } ); + } ).toPass( { timeout: 60000 } ); +} + +async function setQuery( chartEditor, query ) { + await chartEditor + .locator( '#visualizer-db-query .CodeMirror' ) + .evaluate( + ( editor, sql ) => editor.CodeMirror.setValue( sql ), + query + ); +} + +async function showQueryResults( page, chartEditor, expectedValue ) { + const [ response ] = await Promise.all( [ + page.waitForResponse( ( candidate ) => { + const request = candidate.request(); + + return ( + candidate.url().includes( '/wp-admin/admin-ajax.php' ) && + request.method() === 'POST' && + request + .postData() + ?.includes( 'action=visualizer-fetch-db-data' ) + ); + } ), + chartEditor.getByRole( 'button', { name: 'Show Results' } ).click(), + ] ); + + expect( response.ok() ).toBe( true ); + await expect( chartEditor.locator( '.db-wizard-results' ) ).toContainText( + expectedValue + ); +} + +test.describe( 'Database source', () => { + const postIds = []; + + test.beforeEach( async ( { page, requestUtils } ) => { + await deleteAllCharts( requestUtils ); + page.setDefaultTimeout( 30000 ); + page.setDefaultNavigationTimeout( 60000 ); + } ); + + test.afterEach( async ( { requestUtils } ) => { + await Promise.all( + postIds.splice( 0 ).map( ( postId ) => + requestUtils + .rest( { + method: 'DELETE', + path: `/wp/v2/posts/${ postId }`, + params: { force: true }, + } ) + .catch( () => {} ) + ) + ); + } ); + + test( 'saves previewed one-time data when saving the chart', async ( { + admin, + baseURL, + context, + page, + requestUtils, + } ) => { + test.setTimeout( 300000 ); + + await context.addCookies( [ + { + name: 'visualizer_e2e_database_source', + value: '1', + url: baseURL, + }, + ] ); + + const sourcePost = await requestUtils.createPost( { + title: 'ISSUE_1329_OLD', + status: 'publish', + } ); + postIds.push( sourcePost.id ); + + await admin.visitAdminPage( + 'admin.php?page=visualizer&vaction=addnew' + ); + await page + .getByRole( 'button', { name: 'Classic Builder Step-by-step' } ) + .click(); + + const chartEditor = page.frameLocator( 'iframe' ); + await chartEditor + .locator( 'div#type-picker' ) + .waitFor( { state: 'visible', timeout: 30000 } ); + await selectChartAdmin( chartEditor, CHART_JS_LABELS.table ); + + await chartEditor + .locator( '#visualizer-chart-id' ) + .waitFor( { state: 'attached', timeout: 30000 } ); + const chartId = await chartEditor + .locator( '#visualizer-chart-id' ) + .getAttribute( 'data-id' ); + expect( chartId ).toMatch( /^\d+$/ ); + + await chartEditor + .getByRole( 'button', { name: /Import from database/ } ) + .click(); + await openQueryPanel( chartEditor ); + + const query = `SELECT post_title AS value FROM wp_posts WHERE ID = ${ sourcePost.id }`; + await setQuery( chartEditor, query ); + + await showQueryResults( page, chartEditor, 'ISSUE_1329_OLD' ); + + const [ saveResponse ] = await Promise.all( [ + page.waitForResponse( + ( candidate ) => + candidate + .url() + .includes( 'action=visualizer-save-db-query' ) && + candidate.request().method() === 'POST' + ), + chartEditor + .locator( '#db-chart-button' ) + .evaluate( ( button ) => button.click() ), + ] ); + expect( saveResponse.ok() ).toBe( true ); + await expect( chartEditor.locator( '#canvas' ) ).toContainText( + 'ISSUE_1329_OLD', + { timeout: 30000 } + ); + await chartEditor + .getByRole( 'button', { name: 'Create Chart' } ) + .click(); + await expect( + page.getByRole( 'heading', { name: 'Visualizer Library' } ) + ).toBeVisible( { timeout: 30000 } ); + + const chartPost = await requestUtils.createPost( { + title: 'Issue 1329 chart', + content: `[visualizer id="${ chartId }"]`, + status: 'publish', + } ); + postIds.push( chartPost.id ); + + await page.goto( `/?p=${ chartPost.id }` ); + await expectChartCell( page, 'ISSUE_1329_OLD' ); + + await requestUtils.rest( { + method: 'POST', + path: `/wp/v2/posts/${ sourcePost.id }`, + data: { title: 'ISSUE_1329_NEW' }, + } ); + + await admin.visitAdminPage( 'admin.php?page=visualizer' ); + await page + .locator( `.visualizer-chart-edit[data-chart="${ chartId }"]` ) + .click(); + await openQueryPanel( chartEditor ); + await showQueryResults( page, chartEditor, 'ISSUE_1329_NEW' ); + + await chartEditor.getByRole( 'button', { name: 'Save Chart' } ).click(); + await expect( + page.getByRole( 'dialog', { name: 'Visualizer' } ) + ).toHaveCount( 0, { timeout: 30000 } ); + + await page.goto( `/?p=${ chartPost.id }` ); + await expectChartCell( page, 'ISSUE_1329_NEW' ); + + await admin.visitAdminPage( 'admin.php?page=visualizer' ); + await page + .locator( `.visualizer-chart-edit[data-chart="${ chartId }"]` ) + .click(); + await openQueryPanel( chartEditor ); + await setQuery( + chartEditor, + 'SELECT visualizer_missing_column FROM wp_posts' + ); + + const errorDialogPromise = page.waitForEvent( 'dialog' ); + await chartEditor.getByRole( 'button', { name: 'Save Chart' } ).click(); + const errorDialog = await errorDialogPromise; + await errorDialog.dismiss(); + + await expect( + chartEditor.locator( '#visualizer-db-query' ) + ).toBeVisible(); + await expect( + page.getByRole( 'dialog', { name: 'Visualizer' } ) + ).toBeVisible(); + } ); +} );