diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 2e128fe1c..0ecef9ff2 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -23,7 +23,6 @@ ## Checklist - [ ] PR title follows [Conventional Commits](https://www.conventionalcommits.org/) format -- [ ] A changeset has been added (`pnpm changeset`) for any change to a published package - [ ] `pnpm build` passes locally - [ ] No hardcoded credentials, internal URLs, client names, or PII introduced - [ ] Any new dependency has a GPL-2.0-or-later-compatible license (MIT, BSD, Apache-2.0, ISC are all compatible) diff --git a/packages/commons/src/Blocks.jsx b/packages/commons/src/Blocks.jsx index 6983bf7ff..e14ebe1e5 100644 --- a/packages/commons/src/Blocks.jsx +++ b/packages/commons/src/Blocks.jsx @@ -18,6 +18,29 @@ import { getTranslatedOptions } from './APIutils'; import { isSupersetAPI } from "./APIutils"; +// Global iframe load throttle — prevents browser ERR_INSUFFICIENT_RESOURCES when many blocks +// are on the same page and all try to load the Vite dev server simultaneously. +const _iframeQueue = []; +let _iframeLoadingCount = 0; +const MAX_CONCURRENT_IFRAMES = 3; + +function _requestIframeSlot(callback) { + if (_iframeLoadingCount < MAX_CONCURRENT_IFRAMES) { + _iframeLoadingCount++; + callback(); + } else { + _iframeQueue.push(callback); + } +} + +function _releaseIframeSlot() { + _iframeLoadingCount = Math.max(0, _iframeLoadingCount - 1); + if (_iframeQueue.length > 0) { + _iframeLoadingCount++; + _iframeQueue.shift()(); + } +} + export const SizeConfig = ({ height, setAttributes, panelStatus, initialOpen }) => { return ( togglePanel("SIZE", panelStatus, setAttributes)} @@ -42,15 +65,20 @@ export class ComponentWithSettings extends Component { react_ui_url: '' }; - window.addEventListener("message", (event) => { + this.iframe = createRef(); + this._messageHandler = (event) => { if (event.data.type === 'componentReady' && event.data.value === true) { - if (this.iframe.current) { - console.log("-----------Sending message -----------"); + // Only respond to our own iframe to avoid O(N²) postMessage storms + if (this.iframe.current && event.source === this.iframe.current.contentWindow) { + if (this._hasIframeSlot) { + _releaseIframeSlot(); + this._hasIframeSlot = false; + } this.iframe.current.contentWindow.postMessage(({ messageType: 'component-attributes', ...this.props.attributes }), "*"); } } - }, false); - this.iframe = createRef(); + }; + window.addEventListener("message", this._messageHandler, false); this.unsubscribe = wp.data.subscribe(() => { const newPreviewMode = wp.data.select("core/editor").getDeviceType(); if (newPreviewMode !== this.state.previewMode) { @@ -67,25 +95,43 @@ export class ComponentWithSettings extends Component { componentDidMount() { apiFetch({ path: '/dg/v1/settings' }).then((data) => { - this.setState({ - react_ui_url: data["react_ui_url"] + '/' + window._page_locale, + const stateWithoutUrl = { react_api_url: data["react_api_url"], apache_superset_url: data["apache_superset_url"], site_language: data["site_language"], current_language: new URLSearchParams(document.location.search).get("edit_lang"), landing_page_url: data["landing_page_url"] || '' - }, () => { - if (this.onSettingsLoaded) { - this.onSettingsLoaded(); + }; + const url = data["react_ui_url"] + '/' + window._page_locale; + // Set non-iframe state immediately, defer the URL that triggers iframe rendering + this.setState(stateWithoutUrl); + this._hasIframeSlot = true; + _requestIframeSlot(() => { + if (!this._unmounted) { + this.setState({ react_ui_url: url }, () => { + if (this.onSettingsLoaded) { + this.onSettingsLoaded(); + } + }); + } else { + _releaseIframeSlot(); + this._hasIframeSlot = false; } }); }); } componentWillUnmount() { + this._unmounted = true; if (this.unsubscribe) { this.unsubscribe(); } + window.removeEventListener('message', this._messageHandler); + // Release any held slot so the queue doesn't stall + if (this._hasIframeSlot) { + _releaseIframeSlot(); + this._hasIframeSlot = false; + } } renderWordpressSource() { @@ -564,39 +610,56 @@ export class BlockEditWithAPIMetadata extends ComponentWithSettings { label: 'CSV', value: 'csv' }]; - this.setState({ - react_ui_url: settingsData["react_ui_url"] + '/' + window._page_locale, + const url = settingsData["react_ui_url"] + '/' + window._page_locale; + const baseState = { react_api_url: settingsData["react_api_url"], apache_superset_url: settingsData["apache_superset_url"], site_language: settingsData["site_language"], current_language: new URLSearchParams(document.location.search).get("edit_lang"), apps - }, () => { - - const { app, dvzProxyDatasetId } = this.props.attributes; - - if (isSupersetAPI(app, this.state.apps)) { - this.loadDatasets(app); - } - - if (app && app != 'none') { - this.loadMetadata(app, dvzProxyDatasetId); + }; + this.setState(baseState); + this._hasIframeSlot = true; + _requestIframeSlot(() => { + if (!this._unmounted) { + this.setState({ react_ui_url: url }, () => { + const { app, dvzProxyDatasetId } = this.props.attributes; + if (isSupersetAPI(app, this.state.apps)) { + this.loadDatasets(app); + } + if (app && app != 'none') { + this.loadMetadata(app, dvzProxyDatasetId); + } + }); + } else { + _releaseIframeSlot(); + this._hasIframeSlot = false; } }); }) .catch((error) => { console.error("Error when loading apps, falling back to CSV", error); - this.setState({ - react_ui_url: settingsData["react_ui_url"] + '/' + window._page_locale, + const url = settingsData["react_ui_url"] + '/' + window._page_locale; + const baseState = { react_api_url: settingsData["react_api_url"], apache_superset_url: settingsData["apache_superset_url"], site_language: settingsData["site_language"], current_language: new URLSearchParams(document.location.search).get("edit_lang"), apps: [{ label: 'CSV', value: 'csv' }] - }, () => { - const { app, dvzProxyDatasetId } = this.props.attributes; - if (app && app != 'none') { - this.loadMetadata(app, dvzProxyDatasetId); + }; + this.setState(baseState); + this._hasIframeSlot = true; + _requestIframeSlot(() => { + if (!this._unmounted) { + this.setState({ react_ui_url: url }, () => { + const { app, dvzProxyDatasetId } = this.props.attributes; + if (app && app != 'none') { + this.loadMetadata(app, dvzProxyDatasetId); + } + }); + } else { + _releaseIframeSlot(); + this._hasIframeSlot = false; } }); }); diff --git a/plugins/wp-react-blocks-plugin/blocks/big-number-trend/BlockEdit.js b/plugins/wp-react-blocks-plugin/blocks/big-number-trend/BlockEdit.js index ee002b384..b284cd8bd 100644 --- a/plugins/wp-react-blocks-plugin/blocks/big-number-trend/BlockEdit.js +++ b/plugins/wp-react-blocks-plugin/blocks/big-number-trend/BlockEdit.js @@ -30,6 +30,22 @@ class BlockEdit extends BlockEditWithAPIMetadata { super.componentDidMount() } + componentDidUpdate(prevProps, prevState, snapshot) { + super.componentDidUpdate(prevProps, prevState, snapshot); + // The base class sends raw WP attributes, but measures/format/filters/percentChangeFormat + // are Array/Object types that the embeddable expects as JSON strings. Send a correction. + if (this.iframe && this.iframe.current) { + const { measures, format, filters, percentChangeFormat } = this.props.attributes; + this.iframe.current.contentWindow.postMessage({ + messageType: 'component-attributes', + measures: JSON.stringify(measures), + format: JSON.stringify(format), + filters: JSON.stringify(filters), + percentChangeFormat: JSON.stringify(percentChangeFormat), + }, '*'); + } + } + render() { const { className, isSelected, diff --git a/plugins/wp-react-blocks-plugin/blocks/data-filters-reset/BlockEdit.js b/plugins/wp-react-blocks-plugin/blocks/data-filters-reset/BlockEdit.js index d6f14c1e2..3b5d95325 100644 --- a/plugins/wp-react-blocks-plugin/blocks/data-filters-reset/BlockEdit.js +++ b/plugins/wp-react-blocks-plugin/blocks/data-filters-reset/BlockEdit.js @@ -23,7 +23,6 @@ class BlockEdit extends BlockEditWithAPIMetadata { } = this.props; - const queryString = `data-group=${group}&data-app=${app}&data-reset-label=${resetLabel}` const iframeStyles = {height: '30px'} return ([isSelected && ( diff --git a/plugins/wp-react-blocks-plugin/blocks/featured-tabs/BlockEdit.js b/plugins/wp-react-blocks-plugin/blocks/featured-tabs/BlockEdit.js index 459bdebaa..e6aa9c48c 100644 --- a/plugins/wp-react-blocks-plugin/blocks/featured-tabs/BlockEdit.js +++ b/plugins/wp-react-blocks-plugin/blocks/featured-tabs/BlockEdit.js @@ -10,17 +10,10 @@ class BlockEdit extends BlockEditWithFilters { } onChangeColor(i, value) { - const { - setAttributes, - attributes: { - colors - }, - } = this.props; - - const newColors = Object.assign({}, colors); - newColors["color_" + i] = value; - - setAttributes({ "colors": newColors }); + const { setAttributes, attributes: { color } } = this.props; + const parts = (color || '').split(','); + parts[i] = value || '#FFFF'; + setAttributes({ color: parts.join(',') }); } componentWillUnmount() { @@ -30,6 +23,7 @@ class BlockEdit extends BlockEditWithFilters { } componentDidUpdate(prevProps, prevState, snapshot) { + super.componentDidUpdate(prevProps, prevState, snapshot); const newPreviewMode = this.state?.previewMode; if (newPreviewMode !== prevState.previewMode) { this.props.setAttributes({ previewMode: newPreviewMode }); @@ -42,12 +36,12 @@ class BlockEdit extends BlockEditWithFilters { toggleSelection, setAttributes, attributes: { - count, + items, type, taxonomy, categories, height, - colors, + color, useScrolls, readMoreLabel, closeLabel, @@ -55,8 +49,6 @@ class BlockEdit extends BlockEditWithFilters { }, } = this.props; - const colorsParams = Object.keys(colors).map(k => colors[k]).join(","); - const queryString = `editing=true&data-type=${type}&data-taxonomy=${taxonomy}&data-categories=${categories}&data-items=${count}&data-height=${height}&data-color=${encodeURIComponent(colorsParams)}&data-read-more-label=${readMoreLabel}&data-close-label=${encodeURIComponent(closeLabel || 'Close')}&data-use-scrolls=${useScrolls}&data-preview-mode=${previewMode}`; const divStyles = { height: `${height}px`, width: "100%" }; return ( @@ -67,8 +59,8 @@ class BlockEdit extends BlockEditWithFilters { setAttributes({ count })} + value={items} + onChange={(items) => setAttributes({ items })} min={2} max={10} /> @@ -118,13 +110,13 @@ class BlockEdit extends BlockEditWithFilters { {this.renderFilters()} - {new Array(count).fill(1).map((v, i) => + {new Array(items).fill(1).map((v, i) => this.onChangeColor(i, colorValue), label: __('Background Color'), } @@ -164,7 +156,8 @@ class BlockEdit extends BlockEditWithFilters { >
{this.state.react_ui_url &&