diff --git a/src/components/renderer/form-button.vue b/src/components/renderer/form-button.vue index aa311635..45e3a7cd 100644 --- a/src/components/renderer/form-button.vue +++ b/src/components/renderer/form-button.vue @@ -10,7 +10,7 @@ :disabled="showSpinner" > - {{ showSpinner ? (!loadingLabel ? 'Loading...': loadingLabel) : label }} + {{ showSpinner ? (!loadingLabel ? "Loading..." : loadingLabel) : label }} @@ -19,17 +19,31 @@ import Mustache from 'mustache'; import { mapActions, mapState } from "vuex"; import { getValidPath } from '@/mixins'; +import Worker from "@/workers/worker.js?worker&inline"; export default { mixins: [getValidPath], - props: ['variant', 'label', 'event', 'eventData', 'name', 'fieldValue', 'value', 'tooltip', 'transientData', 'loading', 'loadingLabel', 'handler'], + props: [ + "variant", + "label", + "event", + "eventData", + "name", + "fieldValue", + "value", + "tooltip", + "transientData", + "loading", + "loadingLabel", + "handler" + ], data() { return { showSpinner: false }; }, computed: { - ...mapState('globalErrorsModule', ['valid']), + ...mapState("globalErrorsModule", ["valid"]), classList() { let variant = this.variant || 'primary'; return { @@ -106,15 +120,36 @@ export default { async runHandler() { if (this.handler) { try { - const data = this.getScreenDataReference(null, (screen, name, value) => { - // Enable the data reference to be updated by the handler - screen.$set(screen.vdata, name, value); + const data = this.getScreenDataReference( + null, + (screen, name, value) => { + // Enable the data reference to be updated by the handler + screen.$set(screen.vdata, name, value); + } + ); + + const rawData = data[Symbol.for("__v_raw")]; + + const worker = new Worker(); + // Send the handler code to the worker + worker.postMessage({ + fn: this.handler, + data: rawData, }); - await new Function(['toRaw'], this.handler).apply(data, [(item) => { - return item[Symbol.for('__v_raw')]; - }]); + + // Listen for the result from the worker + worker.onmessage = (e) => { + if (e.data.error) { + console.error("Worker error:", e.data.error); + } else if (e.data.result) { + // Update the data with the result + Object.keys(e.data.result).forEach(key => { + rawData[key] = e.data.result[key]; + }); + } + }; } catch (error) { - console.error('❌ There is an error in the button handler', error); + console.error("❌ There is an error in the button handler", error); } } } diff --git a/src/workers/worker.js b/src/workers/worker.js new file mode 100644 index 00000000..ab1eb3fd --- /dev/null +++ b/src/workers/worker.js @@ -0,0 +1,63 @@ +// worker.js +self.onmessage = async function (e) { + const { fn, data } = e.data; + + try { + // Validate inputs + if (!fn || typeof fn !== 'string') { + throw new Error('Function code must be a string'); + } + + // Check if the code is asynchronous + const isAsync = detectAsyncCode(fn); + + // If the code contains await, wrap it in an async function + const functionBody = isAsync + ? `return (async () => { ${fn} })();` + : fn; + + // Use Function constructor with explicit parameter and body + // eslint-disable-next-line no-new-func + const userFunc = new Function('data', functionBody); + const result = isAsync ? await userFunc(data) : userFunc(data); + + self.postMessage({ result }); + } catch (error) { + console.error('Error executing handler:', error); + + self.postMessage({ + error: error.message, + stack: error.stack + }); + } +}; + +function detectAsyncCode(code) { + // Remove comments and strings to avoid false positives + const cleanCode = code + .replace(/\/\*[\s\S]*?\*\//g, '') // Remove block comments + .replace(/\/\/.*$/gm, '') // Remove line comments + .replace(/"[^"]*"/g, '""') // Replace string content + .replace(/'[^']*'/g, "''") // Replace string content + .replace(/`[^`]*`/g, '``'); // Replace template literals + + // Check for async patterns + const asyncPatterns = [ + /\bawait\b/, // await keyword + /\bPromise\b/, // Promise constructor + /\bfetch\b/, // fetch API + /\bsetTimeout\b/, // setTimeout + /\bsetInterval\b/, // setInterval + /\brequestAnimationFrame\b/, // requestAnimationFrame + /\brequestIdleCallback\b/, // requestIdleCallback + /\bnew\s+Promise/, // new Promise + /\b\.then\s*\(/, // .then() method + /\b\.catch\s*\(/, // .catch() method + /\b\.finally\s*\(/, // .finally() method + /\bPromise\./, // Promise static methods + /\basync\b/, // async keyword (in case it's used) + ]; + + // Check if any async pattern is found + return asyncPatterns.some((pattern) => pattern.test(cleanCode)); +} diff --git a/tests/e2e/fixtures/button_click_handler.json b/tests/e2e/fixtures/button_click_handler.json index 504cac5a..6bb22c4a 100644 --- a/tests/e2e/fixtures/button_click_handler.json +++ b/tests/e2e/fixtures/button_click_handler.json @@ -690,7 +690,7 @@ "name": "button_with_handler", "fieldValue": null, "tooltip": {}, - "handler": "this.form_input_2=\"value changed by handler\";" + "handler": "return {\n form_input_2: \"value changed by handler\",\n}" }, "inspector": [ { @@ -1392,7 +1392,7 @@ "name": "record_list_button_with_handler", "fieldValue": null, "tooltip": {}, - "handler": "this.form_input_1=\"value changed by handler\";console.log('handler executed')" + "handler": "console.log('handler executed');\nreturn {\n form_input_1: \"value changed by handler\",\n}" }, "inspector": [ { @@ -1818,4 +1818,4 @@ ], "screen_categories": [], "scripts": [] -} \ No newline at end of file +}