diff --git a/src/background/index.js b/src/background/index.js index 6ea4c2cb4..37609a3c7 100644 --- a/src/background/index.js +++ b/src/background/index.js @@ -191,6 +191,14 @@ message.on('recording:stop', async () => { console.error(error); } }); +message.on('workflow:resume', ({ id, nextBlock }) => { + if (!id) return; + workflowState.resume(id, nextBlock); +}); +message.on('workflow:breakpoint', (id) => { + if (!id) return; + workflowState.update(id, { status: 'breakpoint' }); +}); automa('background', message); diff --git a/src/components/block/BlockBase.vue b/src/components/block/BlockBase.vue index 0d636cb2b..010f08194 100644 --- a/src/components/block/BlockBase.vue +++ b/src/components/block/BlockBase.vue @@ -65,6 +65,15 @@ + @@ -95,6 +104,7 @@ const props = defineProps({ defineEmits(['delete', 'edit', 'update', 'settings']); const isCopied = ref(false); +const workflow = inject('workflow', null); const workflowUtils = inject('workflow-utils', null); function insertToClipboard() { diff --git a/src/components/newtab/workflow/editor/EditorDebugging.vue b/src/components/newtab/workflow/editor/EditorDebugging.vue new file mode 100644 index 000000000..85866e493 --- /dev/null +++ b/src/components/newtab/workflow/editor/EditorDebugging.vue @@ -0,0 +1,169 @@ + + + + + + + + {{ + t( + `common.${ + workflowState.status === 'breakpoint' ? 'resume' : 'pause' + }` + ) + }} + + + + + + + + + + + + + + + {{ getBlockName(block.name) }} + + + + + {{ t('workflow.testing.startRun') }}: + {{ dayjs(block.startedAt).format('HH:mm:ss, SSS') }} + + + + + + + + + + diff --git a/src/components/newtab/workflow/editor/EditorLocalActions.vue b/src/components/newtab/workflow/editor/EditorLocalActions.vue index 4f6e629c1..19dd57c79 100644 --- a/src/components/newtab/workflow/editor/EditorLocalActions.vue +++ b/src/components/newtab/workflow/editor/EditorLocalActions.vue @@ -122,18 +122,35 @@ - - - + + + + + + + + { ); }); -function copyWorkflowId() { - navigator.clipboard.writeText(props.workflow.id).catch((error) => { - console.error(error); - - const textarea = document.createElement('textarea'); - textarea.value = props.workflow.id; - textarea.select(); - document.execCommand('copy'); - textarea.blur(); - }); -} function updateWorkflow(data = {}, changedIndicator = false) { let store = null; @@ -434,6 +440,22 @@ function updateWorkflow(data = {}, changedIndicator = false) { return result; }); } +function toggleTestingMode() { + if (props.isDataChanged) return; + + updateWorkflow({ testingMode: !props.workflow.testingMode }); +} +function copyWorkflowId() { + navigator.clipboard.writeText(props.workflow.id).catch((error) => { + console.error(error); + + const textarea = document.createElement('textarea'); + textarea.value = props.workflow.id; + textarea.select(); + document.execCommand('copy'); + textarea.blur(); + }); +} function updateWorkflowDescription(value) { const keys = ['description', 'category', 'content', 'tag', 'name']; const payload = {}; diff --git a/src/lib/vRemixicon.js b/src/lib/vRemixicon.js index 15466d5ae..432e66a1f 100644 --- a/src/lib/vRemixicon.js +++ b/src/lib/vRemixicon.js @@ -22,6 +22,7 @@ import { riTimeLine, riFlagLine, riFileLine, + riBug2Line, riTeamLine, riLinksLine, riGroupLine, @@ -122,6 +123,7 @@ import { riArrowGoBackLine, riInputCursorMove, riCloseCircleLine, + riRecordCircleFill, riRecordCircleLine, riErrorWarningLine, riExternalLinkLine, @@ -160,6 +162,7 @@ export const icons = { riTimeLine, riFlagLine, riFileLine, + riBug2Line, riTeamLine, riLinksLine, riGroupLine, @@ -260,6 +263,7 @@ export const icons = { riArrowGoBackLine, riInputCursorMove, riCloseCircleLine, + riRecordCircleFill, riRecordCircleLine, riErrorWarningLine, riExternalLinkLine, diff --git a/src/locales/en/common.json b/src/locales/en/common.json index 636fbcf8e..1b6ab5ef1 100644 --- a/src/locales/en/common.json +++ b/src/locales/en/common.json @@ -27,6 +27,8 @@ "data": "data", "stop": "Stop", "sheet": "Sheet", + "pause": "Pause", + "resume": "Resume", "action": "Action | Actions", "packages": "Packages", "storage": "Storage", diff --git a/src/locales/en/newtab.json b/src/locales/en/newtab.json index a6da57c89..b48a038cd 100644 --- a/src/locales/en/newtab.json +++ b/src/locales/en/newtab.json @@ -196,6 +196,12 @@ "preferInTab": "Prefer input parameters in the tab" }, "my": "My workflows", + "testing": { + "title": "Testing mode", + "nextBlock": "Next block", + "startRun": "Start run at", + "disabled": "Save changes first" + }, "import": "Import workflow", "new": "New workflow", "delete": "Delete workflow", diff --git a/src/newtab/App.vue b/src/newtab/App.vue index 395e6c35a..61dcf7109 100644 --- a/src/newtab/App.vue +++ b/src/newtab/App.vue @@ -264,7 +264,6 @@ browser.runtime.onMessage.addListener(({ type, data }) => { browser.storage.local.onChanged.addListener(({ workflowStates }) => { if (!workflowStates) return; - const states = Object.values(workflowStates.newValue); workflowStore.states = states; }); diff --git a/src/newtab/pages/workflows/[id].vue b/src/newtab/pages/workflows/[id].vue index d5573655a..22c484744 100644 --- a/src/newtab/pages/workflows/[id].vue +++ b/src/newtab/pages/workflows/[id].vue @@ -156,6 +156,11 @@ + { + if (this.id !== id || this.isDestroyed) return; + + this.workers.forEach((worker) => { + worker.resume(nextBlock); + }); + }; } async init() { @@ -269,6 +278,7 @@ class WorkflowEngine { this.startedTimestamp = Date.now(); this.states.on('stop', this.onWorkflowStopped); + this.states.on('resume', this.onResumeExecution); const credentials = await dbStorage.credentials.toArray(); credentials.forEach(({ name, value }) => { @@ -284,6 +294,7 @@ class WorkflowEngine { await this.states.add(this.id, { id: this.id, + status: 'running', state: this.state, workflowId: this.workflow.id, parentState: this.parentWorkflow, diff --git a/src/workflowEngine/WorkflowState.js b/src/workflowEngine/WorkflowState.js index f220c6073..25c4e89e5 100644 --- a/src/workflowEngine/WorkflowState.js +++ b/src/workflowEngine/WorkflowState.js @@ -81,8 +81,28 @@ class WorkflowState { return id; } + async resume(id, nextBlock) { + const state = this.states.get(id); + if (!state) return; + + this.states.set(id, { + ...state, + status: 'running', + }); + await this._saveToStorage(); + + this.dispatchEvent('resume', { id, nextBlock }); + } + async update(id, data = {}) { const state = this.states.get(id); + if (!state) return; + + if (data?.state?.status) { + state.status = data.state.status; + delete data.state.status; + } + this.states.set(id, { ...state, ...data }); this.dispatchEvent('update', { id, data }); await this._saveToStorage(); diff --git a/src/workflowEngine/WorkflowWorker.js b/src/workflowEngine/WorkflowWorker.js index fb33f9598..a0712ab4e 100644 --- a/src/workflowEngine/WorkflowWorker.js +++ b/src/workflowEngine/WorkflowWorker.js @@ -46,6 +46,7 @@ class WorkflowWorker { this.loopList = {}; this.repeatedTasks = {}; this.preloadScripts = []; + this.breakpointState = null; this.windowId = null; this.currentBlock = null; @@ -135,13 +136,22 @@ class WorkflowWorker { return [...connections.values()]; } - executeNextBlocks(connections, prevBlockData) { + executeNextBlocks( + connections, + prevBlockData, + nextBlockBreakpointCount = null + ) { connections.forEach((connection, index) => { const { id, targetHandle, sourceHandle } = typeof connection === 'string' ? { id: connection, targetHandle: '', sourceHandle: '' } : connection; - const execParam = { prevBlockData, targetHandle, sourceHandle }; + const execParam = { + prevBlockData, + targetHandle, + sourceHandle, + nextBlockBreakpointCount, + }; if (index === 0) { this.executeBlock(this.engine.blocks[id], { @@ -167,6 +177,19 @@ class WorkflowWorker { }); } + resume(nextBlock) { + if (!this.breakpointState) return; + + const { block, execParam, isRetry } = this.breakpointState; + const payload = { ...execParam, resume: true }; + + payload.nextBlockBreakpointCount = nextBlock ? 1 : null; + + this.executeBlock(block, payload, isRetry); + + this.breakpointState = null; + } + async executeBlock(block, execParam = {}, isRetry = false) { const currentState = await this.engine.states.get(this.engine.id); @@ -181,11 +204,32 @@ class WorkflowWorker { const prevBlock = this.currentBlock; this.currentBlock = { ...block, startedAt: startExecuteTime }; + const isInBreakpoint = + this.engine.isTestingMode && + ((block.data?.$breakpoint && !execParam.resume) || + execParam.nextBlockBreakpointCount === 0); + if (!isRetry) { - await this.engine.updateState({ + const payload = { activeTabUrl: this.activeTab.url, childWorkflowId: this.childWorkflowId, - }); + nextBlockBreakpoint: Boolean(execParam.nextBlockBreakpointCount), + }; + if (isInBreakpoint && currentState.status !== 'breakpoint') + payload.status = 'breakpoint'; + + await this.engine.updateState(payload); + } + + if (execParam.nextBlockBreakpointCount) { + execParam.nextBlockBreakpointCount -= 1; + } + + if (isInBreakpoint || currentState.status === 'breakpoint') { + this.engine.isInBreakpoint = true; + this.breakpointState = { block, execParam, isRetry }; + + return; } const blockHandler = this.engine.blocksHandler[toCamelCase(block.label)]; @@ -238,6 +282,14 @@ class WorkflowWorker { }); }; + const executeBlocks = (blocks, data) => { + return this.executeNextBlocks( + blocks, + data, + execParam.nextBlockBreakpointCount + ); + }; + try { let result; @@ -253,11 +305,6 @@ class WorkflowWorker { ...(execParam || {}), }); result = await blockExecutionWrapper(bindedHandler, block.data); - // result = await handler.call(this, replacedBlock, { - // refData, - // prevBlock, - // ...(execParam || {}), - // }); if (this.engine.isDestroyed) return; @@ -273,7 +320,7 @@ class WorkflowWorker { if (result.nextBlockId && !result.destroyWorker) { setTimeout(() => { - this.executeNextBlocks(result.nextBlockId, result.data); + executeBlocks(result.nextBlockId, result.data); }, blockDelay); } else { this.engine.destroyWorker(this.id); @@ -319,7 +366,7 @@ class WorkflowWorker { if (blockOnError.toDo !== 'error' && nextBlocks) { addBlockLog('error', errorLogData); - this.executeNextBlocks(nextBlocks, prevBlockData); + executeBlocks(nextBlocks, prevBlockData); return; } @@ -335,7 +382,7 @@ class WorkflowWorker { if (onError === 'keep-running' && nodeConnections) { setTimeout(() => { - this.executeNextBlocks(nodeConnections, error.data || ''); + executeBlocks(nodeConnections, error.data || ''); }, blockDelay); } else if (onError === 'restart-workflow' && !this.parentWorkflow) { const restartCount = this.engine.restartWorkersCount[this.id] || 0; diff --git a/src/workflowEngine/blocksHandler/handlerExecuteWorkflow.js b/src/workflowEngine/blocksHandler/handlerExecuteWorkflow.js index 337700e6b..c01e54f4e 100644 --- a/src/workflowEngine/blocksHandler/handlerExecuteWorkflow.js +++ b/src/workflowEngine/blocksHandler/handlerExecuteWorkflow.js @@ -72,6 +72,8 @@ async function executeWorkflow({ id: blockId, data }) { workflow = convertWorkflowData(workflow); const optionsParams = { variables: {} }; + if (workflow.testingMode) workflow.testingMode = false; + if (!isWhitespace(data.globalData)) optionsParams.globalData = data.globalData; diff --git a/src/workflowEngine/index.js b/src/workflowEngine/index.js index a77c4a4b1..cf050a0d2 100644 --- a/src/workflowEngine/index.js +++ b/src/workflowEngine/index.js @@ -2,7 +2,6 @@ import { toRaw } from 'vue'; import browser from 'webextension-polyfill'; import dayjs from '@/lib/dayjs'; -import decryptFlow, { getWorkflowPass } from '@/utils/decryptFlow'; import { parseJSON } from '@/utils/helper'; import { fetchApi } from '@/utils/api'; import { sendMessage } from '@/utils/message'; @@ -45,13 +44,9 @@ export function startWorkflowExec(workflowData, options, isPopup = true) { self.localStorage.setItem('runCounts', JSON.stringify(runCounts)); } - if (workflowData.isProtected) { - const flow = parseJSON(workflowData.drawflow, null); - - if (!flow) { - const pass = getWorkflowPass(workflowData.pass); - - workflowData.drawflow = decryptFlow(workflowData, pass); + if (workflowData.testingMode) { + for (const value of workflowState.states.values()) { + if (value.workflowId === workflowData.id) return null; } }
+ {{ getBlockName(block.name) }} +
+ {{ t('workflow.testing.startRun') }}: + {{ dayjs(block.startedAt).format('HH:mm:ss, SSS') }} +