Showing preview only (2,222K chars total). Download the full file or copy to clipboard to get everything.
Repository: AutomaApp/automa
Branch: main
Commit: a4cbe34a60c9
Files: 515
Total size: 2.0 MB
Directory structure:
gitextract_9m71ul2v/
├── .babelrc
├── .editorconfig
├── .eslintrc.js
├── .github/
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ └── feature_request.md
│ └── dependabot.yml
├── .gitignore
├── .prettierrc
├── .vscode/
│ └── settings.json
├── LICENSE.txt
├── README.md
├── business/
│ └── dev/
│ ├── blocks/
│ │ ├── backgroundHandler/
│ │ │ └── index.js
│ │ ├── contentHandler/
│ │ │ └── index.js
│ │ ├── editComponents/
│ │ │ └── index.js
│ │ └── index.js
│ ├── index.js
│ └── parameters/
│ └── index.js
├── jsconfig.json
├── package.json
├── postcss.config.js
├── secrets.blank.js
├── src/
│ ├── assets/
│ │ └── css/
│ │ ├── drawflow.css
│ │ ├── flow.css
│ │ ├── fonts.css
│ │ ├── style.css
│ │ └── tailwind.css
│ ├── background/
│ │ ├── BackgroundEventsListeners.js
│ │ ├── BackgroundOffscreen.js
│ │ ├── BackgroundUtils.js
│ │ ├── BackgroundWorkflowTriggers.js
│ │ ├── BackgroundWorkflowUtils.js
│ │ └── index.js
│ ├── common/
│ │ └── utils/
│ │ └── constant.js
│ ├── components/
│ │ ├── block/
│ │ │ ├── BlockBase.vue
│ │ │ ├── BlockBasic.vue
│ │ │ ├── BlockBasicWithFallback.vue
│ │ │ ├── BlockConditions.vue
│ │ │ ├── BlockDelay.vue
│ │ │ ├── BlockElementExists.vue
│ │ │ ├── BlockGroup.vue
│ │ │ ├── BlockGroup2.vue
│ │ │ ├── BlockLoopBreakpoint.vue
│ │ │ ├── BlockNote.vue
│ │ │ ├── BlockPackage.vue
│ │ │ └── BlockRepeatTask.vue
│ │ ├── content/
│ │ │ ├── selector/
│ │ │ │ ├── SelectorBlocks.vue
│ │ │ │ ├── SelectorElementList.vue
│ │ │ │ ├── SelectorElementsDetail.vue
│ │ │ │ └── SelectorQuery.vue
│ │ │ └── shared/
│ │ │ ├── SharedElementHighlighter.vue
│ │ │ └── SharedElementSelector.vue
│ │ ├── newtab/
│ │ │ ├── app/
│ │ │ │ ├── AppLogs.vue
│ │ │ │ ├── AppLogsItem.vue
│ │ │ │ ├── AppLogsItemRunning.vue
│ │ │ │ ├── AppLogsItems.vue
│ │ │ │ ├── AppSidebar.vue
│ │ │ │ └── AppSurvey.vue
│ │ │ ├── logs/
│ │ │ │ ├── LogsDataViewer.vue
│ │ │ │ ├── LogsFilters.vue
│ │ │ │ ├── LogsHistory.vue
│ │ │ │ ├── LogsTable.vue
│ │ │ │ └── LogsVariables.vue
│ │ │ ├── package/
│ │ │ │ ├── PackageDetails.vue
│ │ │ │ ├── PackageSettingIOSelect.vue
│ │ │ │ └── PackageSettings.vue
│ │ │ ├── settings/
│ │ │ │ ├── SettingsBackupItems.vue
│ │ │ │ ├── SettingsCloudBackup.vue
│ │ │ │ └── jsBlockWrap.js
│ │ │ ├── shared/
│ │ │ │ ├── SharedCard.vue
│ │ │ │ ├── SharedCodemirror.vue
│ │ │ │ ├── SharedConditionBuilder/
│ │ │ │ │ ├── ConditionBuilderInputs.vue
│ │ │ │ │ └── index.vue
│ │ │ │ ├── SharedElSelectorActions.vue
│ │ │ │ ├── SharedLogsTable.vue
│ │ │ │ ├── SharedPermissionsModal.vue
│ │ │ │ ├── SharedWorkflowState.vue
│ │ │ │ ├── SharedWorkflowTriggers.vue
│ │ │ │ └── SharedWysiwyg.vue
│ │ │ ├── storage/
│ │ │ │ ├── StorageCredentials.vue
│ │ │ │ ├── StorageEditTable.vue
│ │ │ │ ├── StorageTables.vue
│ │ │ │ └── StorageVariables.vue
│ │ │ ├── workflow/
│ │ │ │ ├── WorkflowBlockList.vue
│ │ │ │ ├── WorkflowDataTable.vue
│ │ │ │ ├── WorkflowDetailsCard.vue
│ │ │ │ ├── WorkflowEditBlock.vue
│ │ │ │ ├── WorkflowEditor.vue
│ │ │ │ ├── WorkflowGlobalData.vue
│ │ │ │ ├── WorkflowProtect.vue
│ │ │ │ ├── WorkflowRunning.vue
│ │ │ │ ├── WorkflowSettings.vue
│ │ │ │ ├── WorkflowShare.vue
│ │ │ │ ├── WorkflowShareTeam.vue
│ │ │ │ ├── WorkflowSharedActions.vue
│ │ │ │ ├── edit/
│ │ │ │ │ ├── BlockSetting/
│ │ │ │ │ │ ├── BlockSettingGeneral.vue
│ │ │ │ │ │ ├── BlockSettingLines.vue
│ │ │ │ │ │ └── BlockSettingOnError.vue
│ │ │ │ │ ├── EditAiWorkflow.vue
│ │ │ │ │ ├── EditAttributeValue.vue
│ │ │ │ │ ├── EditAutocomplete.vue
│ │ │ │ │ ├── EditBlockSettings.vue
│ │ │ │ │ ├── EditBrowserEvent.vue
│ │ │ │ │ ├── EditClipboard.vue
│ │ │ │ │ ├── EditCloseTab.vue
│ │ │ │ │ ├── EditConditions.vue
│ │ │ │ │ ├── EditCookie.vue
│ │ │ │ │ ├── EditCreateElement.vue
│ │ │ │ │ ├── EditDataMapping.vue
│ │ │ │ │ ├── EditDelay.vue
│ │ │ │ │ ├── EditDeleteData.vue
│ │ │ │ │ ├── EditElementExists.vue
│ │ │ │ │ ├── EditExecuteWorkflow.vue
│ │ │ │ │ ├── EditExportData.vue
│ │ │ │ │ ├── EditForms.vue
│ │ │ │ │ ├── EditGetText.vue
│ │ │ │ │ ├── EditGoogleDrive.vue
│ │ │ │ │ ├── EditGoogleSheets.vue
│ │ │ │ │ ├── EditGoogleSheetsDrive.vue
│ │ │ │ │ ├── EditHandleDialog.vue
│ │ │ │ │ ├── EditHandleDownload.vue
│ │ │ │ │ ├── EditIncreaseVariable.vue
│ │ │ │ │ ├── EditInsertData.vue
│ │ │ │ │ ├── EditInteractionBase.vue
│ │ │ │ │ ├── EditJavascriptCode.vue
│ │ │ │ │ ├── EditLink.vue
│ │ │ │ │ ├── EditLogData.vue
│ │ │ │ │ ├── EditLoopData.vue
│ │ │ │ │ ├── EditLoopElements.vue
│ │ │ │ │ ├── EditNewTab.vue
│ │ │ │ │ ├── EditNewWindow.vue
│ │ │ │ │ ├── EditNotification.vue
│ │ │ │ │ ├── EditParameterPrompt.vue
│ │ │ │ │ ├── EditPressKey.vue
│ │ │ │ │ ├── EditProxy.vue
│ │ │ │ │ ├── EditRegexVariable.vue
│ │ │ │ │ ├── EditSaveAssets.vue
│ │ │ │ │ ├── EditScrollElement.vue
│ │ │ │ │ ├── EditSliceVariable.vue
│ │ │ │ │ ├── EditSortData.vue
│ │ │ │ │ ├── EditSwitchTab.vue
│ │ │ │ │ ├── EditSwitchTo.vue
│ │ │ │ │ ├── EditTabURL.vue
│ │ │ │ │ ├── EditTakeScreenshot.vue
│ │ │ │ │ ├── EditTrigger.vue
│ │ │ │ │ ├── EditTriggerEvent.vue
│ │ │ │ │ ├── EditUploadFile.vue
│ │ │ │ │ ├── EditWaitConnections.vue
│ │ │ │ │ ├── EditWebhook.vue
│ │ │ │ │ ├── EditWhileLoop.vue
│ │ │ │ │ ├── EditWorkflowParameters.vue
│ │ │ │ │ ├── EditWorkflowState.vue
│ │ │ │ │ ├── InsertWorkflowData.vue
│ │ │ │ │ ├── Parameter/
│ │ │ │ │ │ ├── ParameterCheckboxValue.vue
│ │ │ │ │ │ ├── ParameterInputOptions.vue
│ │ │ │ │ │ ├── ParameterInputValue.vue
│ │ │ │ │ │ └── ParameterJsonValue.vue
│ │ │ │ │ ├── Trigger/
│ │ │ │ │ │ ├── TriggerContextMenu.vue
│ │ │ │ │ │ ├── TriggerCronJob.vue
│ │ │ │ │ │ ├── TriggerDate.vue
│ │ │ │ │ │ ├── TriggerElementChange.vue
│ │ │ │ │ │ ├── TriggerElementOptions.vue
│ │ │ │ │ │ ├── TriggerInterval.vue
│ │ │ │ │ │ ├── TriggerKeyboardShortcut.vue
│ │ │ │ │ │ ├── TriggerSpecificDay.vue
│ │ │ │ │ │ └── TriggerVisitWeb.vue
│ │ │ │ │ └── TriggerEvent/
│ │ │ │ │ ├── TriggerEventInput.vue
│ │ │ │ │ ├── TriggerEventKeyboard.vue
│ │ │ │ │ ├── TriggerEventMouse.vue
│ │ │ │ │ ├── TriggerEventTouch.vue
│ │ │ │ │ └── TriggerEventWheel.vue
│ │ │ │ ├── editor/
│ │ │ │ │ ├── EditorAddPackage.vue
│ │ │ │ │ ├── EditorCustomEdge.vue
│ │ │ │ │ ├── EditorDebugging.vue
│ │ │ │ │ ├── EditorLocalActions.vue
│ │ │ │ │ ├── EditorLocalCtxMenu.vue
│ │ │ │ │ ├── EditorLocalSavedBlocks.vue
│ │ │ │ │ ├── EditorLogs.vue
│ │ │ │ │ ├── EditorPkgActions.vue
│ │ │ │ │ ├── EditorSearchBlocks.vue
│ │ │ │ │ └── EditorUsedCredentials.vue
│ │ │ │ └── settings/
│ │ │ │ ├── SettingsBlocks.vue
│ │ │ │ ├── SettingsEvents.vue
│ │ │ │ ├── SettingsGeneral.vue
│ │ │ │ ├── SettingsTable.vue
│ │ │ │ └── event/
│ │ │ │ ├── EventCodeAction.vue
│ │ │ │ └── EventCodeHTTP.vue
│ │ │ └── workflows/
│ │ │ ├── WorkflowsFolder.vue
│ │ │ ├── WorkflowsHosted.vue
│ │ │ ├── WorkflowsLocal.vue
│ │ │ ├── WorkflowsLocalCard.vue
│ │ │ ├── WorkflowsShared.vue
│ │ │ └── WorkflowsUserTeam.vue
│ │ ├── popup/
│ │ │ └── home/
│ │ │ ├── HomeSelectBlock.vue
│ │ │ ├── HomeStartRecording.vue
│ │ │ ├── HomeTeamWorkflows.vue
│ │ │ └── HomeWorkflowCard.vue
│ │ ├── transitions/
│ │ │ ├── TransitionExpand.vue
│ │ │ └── TransitionSlide.vue
│ │ └── ui/
│ │ ├── UiAutocomplete.vue
│ │ ├── UiButton.vue
│ │ ├── UiCard.vue
│ │ ├── UiCheckbox.vue
│ │ ├── UiDialog.vue
│ │ ├── UiExpand.vue
│ │ ├── UiFileInput.vue
│ │ ├── UiImg.vue
│ │ ├── UiInput.vue
│ │ ├── UiList.vue
│ │ ├── UiListItem.vue
│ │ ├── UiModal.vue
│ │ ├── UiPaginatedSelect.vue
│ │ ├── UiPagination.vue
│ │ ├── UiPopover.vue
│ │ ├── UiRadio.vue
│ │ ├── UiSelect.vue
│ │ ├── UiSpinner.vue
│ │ ├── UiSwitch.vue
│ │ ├── UiTab.vue
│ │ ├── UiTabPanel.vue
│ │ ├── UiTabPanels.vue
│ │ ├── UiTable.vue
│ │ ├── UiTabs.vue
│ │ └── UiTextarea.vue
│ ├── composable/
│ │ ├── blockValidation.js
│ │ ├── commandManager.js
│ │ ├── componentId.js
│ │ ├── dialog.js
│ │ ├── editorBlock.js
│ │ ├── groupTooltip.js
│ │ ├── hasPermissions.js
│ │ ├── liveQuery.js
│ │ ├── shortcut.js
│ │ └── theme.js
│ ├── content/
│ │ ├── blocksHandler/
│ │ │ ├── handlerAttributeValue.js
│ │ │ ├── handlerClipboard.js
│ │ │ ├── handlerConditions.js
│ │ │ ├── handlerCreateElement.js
│ │ │ ├── handlerElementExists.js
│ │ │ ├── handlerElementScroll.js
│ │ │ ├── handlerEventClick.js
│ │ │ ├── handlerForms.js
│ │ │ ├── handlerGetText.js
│ │ │ ├── handlerHoverElement.js
│ │ │ ├── handlerJavascriptCode.js
│ │ │ ├── handlerLink.js
│ │ │ ├── handlerLoopData.js
│ │ │ ├── handlerLoopElements.js
│ │ │ ├── handlerPressKey.js
│ │ │ ├── handlerSaveAssets.js
│ │ │ ├── handlerSwitchTo.js
│ │ │ ├── handlerTakeScreenshot.js
│ │ │ ├── handlerTriggerEvent.js
│ │ │ ├── handlerUploadFile.js
│ │ │ └── handlerVerifySelector.js
│ │ ├── blocksHandler.js
│ │ ├── commandPalette/
│ │ │ ├── App.vue
│ │ │ ├── compsUi.js
│ │ │ ├── icons.js
│ │ │ ├── index.js
│ │ │ └── main.js
│ │ ├── elementObserver.js
│ │ ├── elementSelector/
│ │ │ ├── App.vue
│ │ │ ├── compsUi.js
│ │ │ ├── generateElementsSelector.js
│ │ │ ├── getSelectorOptions.js
│ │ │ ├── icons.js
│ │ │ ├── index.js
│ │ │ ├── listSelector.js
│ │ │ ├── main.js
│ │ │ ├── selectorFrameContext.js
│ │ │ └── vueI18n.js
│ │ ├── handleSelector.js
│ │ ├── index.js
│ │ ├── injectAppStyles.js
│ │ ├── services/
│ │ │ ├── recordWorkflow/
│ │ │ │ ├── App.vue
│ │ │ │ ├── addBlock.js
│ │ │ │ ├── icons.js
│ │ │ │ ├── index.js
│ │ │ │ ├── main.js
│ │ │ │ └── recordEvents.js
│ │ │ ├── shortcutListener.js
│ │ │ └── webService.js
│ │ ├── showExecutedBlock.js
│ │ ├── synchronizedLock.js
│ │ └── utils.js
│ ├── db/
│ │ ├── logs.js
│ │ └── storage.js
│ ├── directives/
│ │ ├── VAutofocus.js
│ │ ├── VClosePopover.js
│ │ └── VTooltip.js
│ ├── execute/
│ │ ├── index.html
│ │ └── index.js
│ ├── lib/
│ │ ├── compsUi.js
│ │ ├── cronstrue.js
│ │ ├── dayjs.js
│ │ ├── findSelector.js
│ │ ├── mitt.js
│ │ ├── pinia.js
│ │ ├── query-selector-shadow-dom/
│ │ │ ├── index.js
│ │ │ └── normalize.js
│ │ ├── tippy.js
│ │ ├── tmpl.js
│ │ ├── vRemixicon.js
│ │ ├── vue-toastification.js
│ │ └── vueI18n.js
│ ├── locales/
│ │ ├── en/
│ │ │ ├── blocks.json
│ │ │ ├── common.json
│ │ │ ├── newtab.json
│ │ │ └── popup.json
│ │ ├── es/
│ │ │ ├── blocks.json
│ │ │ ├── common.json
│ │ │ ├── newtab.json
│ │ │ └── popup.json
│ │ ├── fr/
│ │ │ ├── blocks.json
│ │ │ ├── common.json
│ │ │ ├── newtab.json
│ │ │ └── popup.json
│ │ ├── it/
│ │ │ ├── blocks.json
│ │ │ ├── common.json
│ │ │ ├── newtab.json
│ │ │ └── popup.json
│ │ ├── pt-BR/
│ │ │ ├── blocks.json
│ │ │ ├── common.json
│ │ │ ├── newtab.json
│ │ │ └── popup.json
│ │ ├── tr/
│ │ │ ├── blocks.json
│ │ │ ├── common.json
│ │ │ ├── newtab.json
│ │ │ └── popup.json
│ │ ├── uk/
│ │ │ ├── blocks.json
│ │ │ ├── common.json
│ │ │ ├── newtab.json
│ │ │ └── popup.json
│ │ ├── vi/
│ │ │ ├── blocks.json
│ │ │ ├── common.json
│ │ │ ├── newtab.json
│ │ │ └── popup.json
│ │ ├── zh/
│ │ │ ├── blocks.json
│ │ │ ├── common.json
│ │ │ ├── newtab.json
│ │ │ └── popup.json
│ │ └── zh-TW/
│ │ ├── blocks.json
│ │ ├── common.json
│ │ ├── newtab.json
│ │ └── popup.json
│ ├── manifest.chrome.dev.json
│ ├── manifest.chrome.json
│ ├── manifest.firefox.json
│ ├── newtab/
│ │ ├── App.vue
│ │ ├── index.html
│ │ ├── index.js
│ │ ├── pages/
│ │ │ ├── Packages.vue
│ │ │ ├── Recording.vue
│ │ │ ├── ScheduledWorkflow.vue
│ │ │ ├── Settings.vue
│ │ │ ├── Storage.vue
│ │ │ ├── Welcome.vue
│ │ │ ├── Workflows.vue
│ │ │ ├── logs/
│ │ │ │ └── [id].vue
│ │ │ ├── settings/
│ │ │ │ ├── SettingsAbout.vue
│ │ │ │ ├── SettingsBackup.vue
│ │ │ │ ├── SettingsEditor.vue
│ │ │ │ ├── SettingsIndex.vue
│ │ │ │ ├── SettingsProfile.vue
│ │ │ │ └── SettingsShortcuts.vue
│ │ │ ├── storage/
│ │ │ │ └── Tables.vue
│ │ │ └── workflows/
│ │ │ ├── Host.vue
│ │ │ ├── Shared.vue
│ │ │ ├── [id].vue
│ │ │ └── index.vue
│ │ ├── router.js
│ │ └── utils/
│ │ ├── RecordWorkflowUtils.js
│ │ ├── blocksValidation.js
│ │ ├── elementSelector.js
│ │ └── startRecordWorkflow.js
│ ├── offscreen/
│ │ ├── index.html
│ │ ├── index.js
│ │ └── message-listener.js
│ ├── params/
│ │ ├── App.vue
│ │ ├── index.html
│ │ └── index.js
│ ├── popup/
│ │ ├── App.vue
│ │ ├── index.html
│ │ ├── index.js
│ │ ├── pages/
│ │ │ └── Home.vue
│ │ └── router.js
│ ├── sandbox/
│ │ ├── index.html
│ │ ├── index.js
│ │ └── utils/
│ │ ├── handleBlockExpression.js
│ │ ├── handleConditionCode.js
│ │ └── handleJavascriptBlock.js
│ ├── service/
│ │ ├── browser-api/
│ │ │ ├── BrowserAPIEventHandler.js
│ │ │ ├── BrowserAPIService.js
│ │ │ └── browser-api-map.js
│ │ └── renderer/
│ │ └── RendererWorkflowService.js
│ ├── stores/
│ │ ├── folder.js
│ │ ├── hostedWorkflow.js
│ │ ├── main.js
│ │ ├── package.js
│ │ ├── sharedWorkflow.js
│ │ ├── teamWorkflow.js
│ │ ├── user.js
│ │ └── workflow.js
│ ├── utils/
│ │ ├── FindElement.js
│ │ ├── USKeyboardLayout.js
│ │ ├── api.js
│ │ ├── callbackBridge.js
│ │ ├── codeEditorAutocomplete.js
│ │ ├── compareBlockValue.js
│ │ ├── constants/
│ │ │ └── table.js
│ │ ├── convertWorkflowData.js
│ │ ├── credentialUtil.js
│ │ ├── dataExporter.js
│ │ ├── dataMigration.js
│ │ ├── decryptFlow.js
│ │ ├── editor/
│ │ │ ├── DroppedNode.js
│ │ │ ├── EditorCommands.js
│ │ │ └── editorAutocomplete.js
│ │ ├── firstWorkflows.js
│ │ ├── getAIPoweredInfo.js
│ │ ├── getBlockMessage.js
│ │ ├── getFile.js
│ │ ├── getSharedData.js
│ │ ├── getTranslateLog.js
│ │ ├── googleSheetsApi.js
│ │ ├── handleFormElement.js
│ │ ├── helper.js
│ │ ├── message.js
│ │ ├── openGDriveFilePicker.js
│ │ ├── recordKeys.js
│ │ ├── serialization.js
│ │ ├── shared.js
│ │ ├── simulateEvent/
│ │ │ ├── index.js
│ │ │ └── mouseEvent.js
│ │ ├── triggerText.js
│ │ ├── workflowData.js
│ │ └── workflowTrigger.js
│ └── workflowEngine/
│ ├── WorkflowEngine.js
│ ├── WorkflowLogger.js
│ ├── WorkflowManager.js
│ ├── WorkflowState.js
│ ├── WorkflowWorker.js
│ ├── blocksHandler/
│ │ ├── handlerActiveTab.js
│ │ ├── handlerAiWorkflow.js
│ │ ├── handlerBlockPackage.js
│ │ ├── handlerBlocksGroup.js
│ │ ├── handlerBrowserEvent.js
│ │ ├── handlerClipboard.js
│ │ ├── handlerCloseTab.js
│ │ ├── handlerConditions.js
│ │ ├── handlerCookie.js
│ │ ├── handlerCreateElement.js
│ │ ├── handlerDataMapping.js
│ │ ├── handlerDelay.js
│ │ ├── handlerDeleteData.js
│ │ ├── handlerElementExists.js
│ │ ├── handlerExecuteWorkflow.js
│ │ ├── handlerExportData.js
│ │ ├── handlerForwardPage.js
│ │ ├── handlerGoBack.js
│ │ ├── handlerGoogleDrive.js
│ │ ├── handlerGoogleSheets.js
│ │ ├── handlerGoogleSheetsDrive.js
│ │ ├── handlerHandleDialog.js
│ │ ├── handlerHandleDownload.js
│ │ ├── handlerHoverElement.js
│ │ ├── handlerIncreaseVariable.js
│ │ ├── handlerInsertData.js
│ │ ├── handlerInteractionBlock.js
│ │ ├── handlerJavascriptCode.js
│ │ ├── handlerLink.js
│ │ ├── handlerLogData.js
│ │ ├── handlerLoopBreakpoint.js
│ │ ├── handlerLoopData.js
│ │ ├── handlerLoopElements.js
│ │ ├── handlerNewTab.js
│ │ ├── handlerNewWindow.js
│ │ ├── handlerNotification.js
│ │ ├── handlerParameterPrompt.js
│ │ ├── handlerProxy.js
│ │ ├── handlerRegexVariable.js
│ │ ├── handlerReloadTab.js
│ │ ├── handlerRepeatTask.js
│ │ ├── handlerSaveAssets.js
│ │ ├── handlerSliceVariable.js
│ │ ├── handlerSortData.js
│ │ ├── handlerSwitchTab.js
│ │ ├── handlerSwitchTo.js
│ │ ├── handlerTabUrl.js
│ │ ├── handlerTakeScreenshot.js
│ │ ├── handlerTrigger.js
│ │ ├── handlerWaitConnections.js
│ │ ├── handlerWebhook.js
│ │ ├── handlerWhileLoop.js
│ │ └── handlerWorkflowState.js
│ ├── blocksHandler.js
│ ├── helper.js
│ ├── injectContentScript.js
│ ├── templating/
│ │ ├── index.js
│ │ ├── mustacheReplacer.js
│ │ ├── renderString.js
│ │ └── templatingFunctions.js
│ ├── utils/
│ │ ├── conditionCode.js
│ │ ├── javascriptBlockUtil.js
│ │ ├── testConditions.js
│ │ └── webhookUtil.js
│ └── workflowEvent.js
├── tailwind.config.js
├── utils/
│ ├── build-zip.js
│ ├── build.js
│ ├── clean-build-cache.js
│ ├── env.js
│ └── webserver.js
└── webpack.config.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .babelrc
================================================
{
"plugins": [],
"presets": [
["@babel/preset-env", {
"useBuiltIns": "usage",
"corejs": 3,
"targets": {
"browsers": "last 2 Chrome versions"
}
}]
]
}
================================================
FILE: .editorconfig
================================================
# EditorConfig is awesome: http://EditorConfig.org
# https://github.com/jokeyrhyme/standard-editorconfig
# top-most EditorConfig file
root = true
# defaults
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_size = 2
indent_style = space
[*.md]
trim_trailing_whitespace = false
================================================
FILE: .eslintrc.js
================================================
// https://eslint.org/docs/user-guide/configuring
// File taken from https://github.com/vuejs-templates/webpack/blob/1.3.1/template/.eslintrc.js, thanks.
module.exports = {
root: true,
parserOptions: {
parser: '@babel/eslint-parser',
},
env: {
browser: true,
webextensions: true,
},
ignorePatterns: ['src/lib/google-*'],
// https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention
// consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules.
extends: [
'plugin:vue/vue3-recommended',
'airbnb-base',
'plugin:prettier/recommended',
],
// required to lint *.vue files
plugins: ['vue'],
// check if imports actually resolve
settings: {
'import/resolver': {
webpack: {
config: './webpack.config.js',
},
},
},
// add your custom rules here
globals: {
BROWSER_TYPE: true,
},
rules: {
camelcase: 'off',
'no-await-in-loop': 'off',
'no-alert': 'off',
'import/no-import-module-exports': 'off',
'no-console': ['warn', { allow: ['warn', 'error'] }],
'no-underscore-dangle': 'off',
'func-names': 'off',
'vue/v-on-event-hyphenation': 'off',
'import/no-named-default': 'off',
'no-restricted-syntax': 'off',
'vue/multi-word-component-names': 'off',
'prettier/prettier': [
'error',
{
endOfLine: 'auto',
},
],
'import/extensions': [
'error',
'always',
{
js: 'never',
},
],
// disallow reassignment of function parameters
// disallow parameter object manipulation except for specific exclusions
'no-param-reassign': 'off',
'import/no-extraneous-dependencies': 'off',
// disallow default export over named export
'import/prefer-default-export': 'off',
// allow debugger during development
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
},
};
================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms
github: AutomaApp
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. Windows]
- Browser: [e.g. Google Chrome]
- Extension Version: [e.g. v0.12.0]
**Additional context**
Add any other context about the problem here.
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
target-branch: "dev"
schedule:
interval: "daily"
================================================
FILE: .gitignore
================================================
# See https://help.github.com/ignore-files/ for more about ignoring files.
# dependencies
/node_modules
# testing
/coverage
# production
/build
/build-zip
dist
# misc
.DS_Store
.eslintcache
.env.local
.env.development.local
.env.test.local
.env.production.local
.history
*.log
# secrets
secrets.production.js
secrets.development.js
get-pass-key.js
getPassKey.js
/business/prod
/business/test
.idea
================================================
FILE: .prettierrc
================================================
{
"singleQuote": true,
"trailingComma": "es5",
"arrowParens": "always"
}
================================================
FILE: .vscode/settings.json
================================================
{
"i18n-ally.localesPaths": [
"src/locales"
],
"i18n-ally.keystyle": "nested"
}
================================================
FILE: LICENSE.txt
================================================
Source code in this repository is variously licensed under the GNU Affero General Public License (AGPL), or the Automa Commercial License (https://extension.automa.site/license/commercial/).
* Outside of the top-level "business" directory, source code in a given file is licensed under the AGPL.
* Within the the top-level "business" directory, source code in a given file is licensed under the Automa Commercial License, unless otherwise noted.
When built, binary files are generated for the AGPL source code and the Automa Commercial License source code. Binaries located at business.automa.site are released under the Automa Commercial License. Binaries located at all non-business paths are released under the AGPL.
================================================
FILE: README.md
================================================
<img src="src/assets/images/icon-128.png" width="64"/>
# Automa
<p>
<img alt="Automa latest version" src="https://img.shields.io/github/package-json/v/AutomaApp/automa" />
<a href="https://twitter.com/AutomaApp">
<img alt="Follow Us on Twitter" src="https://img.shields.io/twitter/follow/AutomaApp?style=social" />
</a>
<a href="https://discord.gg/C6khwwTE84">
<img alt="Chat with us on Discord" src="https://img.shields.io/discord/942211415517835354?label=join%20discord&logo=Discord&logoColor=white" />
</a>
</p>
An extension for automating your browser by connecting blocks. <br />
Auto-fill forms, do a repetitive task, take a screenshot, or scrape website data — the choice is yours. You can even schedule when the automation will execute!
## Downloads
<table cellspacing="0" cellpadding="0">
<tr>
<td valign="center">
<a align="center" href="https://chrome.google.com/webstore/detail/automa/infppggnoaenmfagbfknfkancpbljcca">
<img src="https://user-images.githubusercontent.com/22908993/166417152-f870bfbd-1770-4c28-b69d-a7303aebc9a6.png" alt="Chrome web store" />
<p align="center">Chrome Web Store</p>
</a>
</td>
<td valign="center">
<a href="https://addons.mozilla.org/en-US/firefox/addon/automa/">
<img src="https://user-images.githubusercontent.com/22908993/166417727-3481fef4-00e5-4cf0-bb03-27fb880d993c.png" alt="Firefox add-ons" />
<p align="center">Firefox Add-ons</p>
</a>
</td>
</tr>
</table>
## Marketplace
Browse the Automa marketplace where you can share and download workflows with others. [Go to the marketplace »](https://extension.automa.site/marketplace)
## Automa Chrome Extension Builder
Automa Chrome Extension Builder (Automa CEB for short) allows you to generate a standalone chrome extension based on Automa workflows. [Go to the documentation »](https://docs.extension.automa.site/extension-builder)
## Project setup
Before running the `yarn dev` or `yarn build` script, you need to create the `getPassKey.js` file in the `src/utils` directory. Inside the file write
```js
export default function() {
return 'anything-you-want';
}
```
```bash
# Install dependencies
pnpm install
# Compiles and hot-reloads for development for the chrome browser
pnpm dev
# Compiles and minifies for production for the chrome browser
pnpm build
# Create a zip file from the build folder
pnpm build:zip
# Compiles and hot-reloads for development for the firefox browser
pnpm dev:firefox
# Compiles and minifies for production for the firefox browser
pnpm build:firefox
# Lints and fixes files
pnpm lint
```
### Icon Preview
v-remixicon/icons: https://preview-v-remixicon.vercel.app/
### Install Locally
#### Chrome
1. Open chrome and navigate to extensions page using this URL: chrome://extensions.
2. Enable the "Developer mode".
3. Click "Load unpacked extension" button, browse the `automa/build` directory and select it.

### Firefox
1. Open firefox and navigate to `about:debugging#/runtime/this-firefox`.
2. Click the "Load Temporary Add-on" button.
3. Browse the `automa/build` directory and select the `manifest.json` file.

## Contributors
Thanks to everyone who has submitted issues, made suggestions, and generally helped make this a better project.
<a href="https://github.com/AutomaApp/automa/graphs/contributors">
<img src="https://contrib.rocks/image?repo=AutomaApp/automa" />
</a>
## License
Source code in this repository is variously licensed under the GNU Affero General Public License (AGPL), or the [Automa Commercial License](https://extension.automa.site/license/commercial/).
See [LICENSE.txt](./LICENSE.txt) for details.
================================================
FILE: business/dev/blocks/backgroundHandler/index.js
================================================
export default function () {
return {};
}
================================================
FILE: business/dev/blocks/contentHandler/index.js
================================================
export default function () {
return {};
}
================================================
FILE: business/dev/blocks/editComponents/index.js
================================================
export default function () {
return {};
}
================================================
FILE: business/dev/blocks/index.js
================================================
export default function () {
return {};
}
================================================
FILE: business/dev/index.js
================================================
export default function () {}
================================================
FILE: business/dev/parameters/index.js
================================================
export default function () {
return {};
}
================================================
FILE: jsconfig.json
================================================
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/*": ["src/*"],
"@business": ["business/dev/*"]
},
"lib": ["ESNext", "DOM"],
"module": "ESNext",
"target": "ES2020"
},
"include": ["src/**/*", "utils/**/*"]
}
================================================
FILE: package.json
================================================
{
"name": "automa",
"version": "1.30.00",
"description": "An extension for automating your browser by connecting blocks",
"repository": {
"type": "git",
"url": "https://github.com/AutomaApp/automa.git"
},
"scripts": {
"build": "node utils/build.js",
"build:firefox": "cross-env BROWSER=firefox npm run build",
"build:zip": "node utils/build-zip.js",
"build:prod": "npm run build:prod-chrome && npm run build:prod-firefox",
"build:prod-chrome": "node utils/clean-build-cache.js && npm run build && npm run build:zip",
"build:prod-firefox": "npm run build:firefox && cross-env BROWSER=firefox npm run build:zip",
"dev": "node utils/webserver.js",
"dev:firefox": "cross-env BROWSER=firefox npm run dev",
"prettier": "prettier --write '**/*.{js,jsx,css,html}'",
"lint": "eslint --ext .js,.vue --ignore-path .gitignore ."
},
"engines": {
"node": ">=14.18.1"
},
"simple-git-hooks": {
"pre-commit": "npx lint-staged"
},
"lint-staged": {
"*.{js,ts,vue}": "eslint --fix"
},
"dependencies": {
"@codemirror/autocomplete": "^6.9.0",
"@codemirror/commands": "^6.6.1",
"@codemirror/lang-css": "^6.2.1",
"@codemirror/lang-html": "^6.4.6",
"@codemirror/lang-javascript": "^6.2.1",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/language": "^6.9.0",
"@codemirror/state": "^6.4.1",
"@codemirror/theme-one-dark": "^6.1.2",
"@codemirror/view": "^6.33.0",
"@medv/finder": "^3.1.0",
"@n8n_io/riot-tmpl": "^2.0.0",
"@tiptap/core": "^2.0.4",
"@tiptap/extension-character-count": "^2.0.4",
"@tiptap/extension-history": "^2.0.4",
"@tiptap/extension-image": "^2.0.4",
"@tiptap/extension-link": "^2.0.4",
"@tiptap/extension-placeholder": "^2.0.4",
"@tiptap/pm": "^2.0.4",
"@tiptap/starter-kit": "^2.0.4",
"@tiptap/vue-3": "^2.0.4",
"@viselect/vanilla": "^3.5.0",
"@vue-flow/background": "^1.2.0",
"@vue-flow/core": "^1.23.0",
"@vue-flow/minimap": "^1.2.0",
"@vueuse/head": "^1.3.1",
"@vueuse/rxjs": "^9.12.0",
"@vuex-orm/core": "^0.36.4",
"codemirror": "^6.0.1",
"compare-versions": "^6.0.0-rc.1",
"cron-parser": "^4.6.0",
"cronstrue": "^2.21.0",
"crypto-js": "4.2.0",
"css-selector-generator": "^3.6.4",
"dagre": "^0.8.5",
"dayjs": "^1.11.6",
"defu": "^6.1.2",
"dexie": "^3.2.3",
"html2canvas": "^1.4.1",
"idb": "^7.0.2",
"js-base64": "^3.7.5",
"json5": "^2.2.3",
"jsonpath": "^1.1.1",
"jspdf": "^2.5.1",
"loader-utils": "^3.2.1",
"lodash.clonedeep": "^4.5.0",
"lodash.merge": "^4.6.2",
"mitt": "^3.0.0",
"mousetrap": "^1.6.5",
"nanoid": "^4.0.0",
"object-path": "^0.11.8",
"papaparse": "^5.3.1",
"pinia": "^2.0.29",
"prosemirror-commands": "^1.5.0",
"prosemirror-dropcursor": "^1.6.1",
"prosemirror-gapcursor": "^1.3.1",
"prosemirror-history": "^1.3.0",
"prosemirror-keymap": "^1.2.0",
"prosemirror-schema-list": "^1.2.2",
"rxjs": "^7.8.0",
"sizzle": "^2.3.8",
"tippy.js": "^6.3.1",
"v-remixicon": "^0.1.1",
"vue": "3.4.38",
"vue-i18n": "^9.14.5",
"vue-imask": "^6.4.2",
"vue-router": "^4.2.4",
"vue-slider-component": "^4.1.0-beta.7",
"vue-toastification": "^2.0.0-rc.5",
"vuedraggable": "^4.1.0",
"vuex": "^4.0.2",
"webextension-polyfill": "^0.12.0",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.19.1/xlsx-0.19.1.tgz"
},
"devDependencies": {
"@babel/core": "^7.20.7",
"@babel/eslint-parser": "^7.18.2",
"@babel/preset-env": "^7.20.2",
"@intlify/vue-i18n-loader": "^4.2.0",
"@tailwindcss/typography": "^0.5.1",
"@types/chrome": "^0.0.267",
"@vue/compiler-sfc": "^3.3.4",
"archiver": "^5.3.1",
"autoprefixer": "^10.4.12",
"babel-loader": "^9.1.2",
"clean-webpack-plugin": "4.0.0",
"copy-webpack-plugin": "^11.0.0",
"core-js": "^3.27.2",
"cross-env": "^7.0.3",
"css-loader": "^6.7.3",
"eslint": "^8.34.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-prettier": "^8.6.0",
"eslint-friendly-formatter": "^4.0.1",
"eslint-import-resolver-webpack": "^0.13.2",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-vue": "^9.4.0",
"file-loader": "^6.2.0",
"fs-extra": "^11.1.0",
"html-loader": "^4.2.0",
"html-webpack-plugin": "^5.5.0",
"lint-staged": "^13.0.2",
"mini-css-extract-plugin": "^2.3.0",
"postcss": "^8.4.21",
"postcss-loader": "^7.0.0",
"prettier": "^2.8.2",
"simple-git-hooks": "^2.8.1",
"source-map-loader": "^4.0.0",
"tailwindcss": "^3.2.1",
"terser-webpack-plugin": "^5.3.6",
"vue-loader": "^17.2.2",
"web-worker": "^1.2.0",
"webpack": "5.76.0",
"webpack-cli": "^5.0.1",
"webpack-dev-server": "^4.11.1"
},
"volta": {
"node": "20.11.1"
}
}
================================================
FILE: postcss.config.js
================================================
module.exports = {
plugins: {
'tailwindcss/nesting': {},
tailwindcss: {},
autoprefixer: {},
},
};
================================================
FILE: secrets.blank.js
================================================
export default {
baseApiUrl: '',
};
================================================
FILE: src/assets/css/drawflow.css
================================================
.drawflow-node.selected-list .menu,
.drawflow-node.selected .menu,
.drawflow-node .block-base:hover .menu {
@apply translate-y-11;
}
.drawflow,
.drawflow .parent-node {
position: relative
}
.parent-drawflow {
display: flex;
overflow: hidden;
touch-action: none;
outline: none;
}
.drawflow {
width: 100%;
height: 100%;
user-select: none;
perspective: 0;
}
.drawflow .drawflow-node {
position: absolute;
align-items: center;
background: white;
min-width: 150px;
min-height: 40px;
z-index: 2;
cursor: move;
white-space: nowrap;
@apply rounded-lg transition ring-2 ring-transparent duration-200 shadow-lg;
}
.drawflow .drawflow-node.selected,
.drawflow .drawflow-node.selected-list {
@apply ring-accent;
}
.drawflow .drawflow-node .inputs,
.drawflow .drawflow-node .outputs {
z-index: 20;
position: absolute;
top: 50%;
transform: translateY(-50%);
}
.drawflow .drawflow-node .inputs {
left: -22px;
}
.drawflow .drawflow-node .outputs {
right: -22px;
}
.drawflow .drawflow-node .drawflow_content_node {
width: 100%;
display: block
}
.drawflow .drawflow-node .input,
.drawflow .drawflow-node .output {
position: relative;
width: 18px;
height: 18px;
border-radius: 50%;
cursor: crosshair;
z-index: 1;
margin-bottom: 5px;
border-width: 3px;
@apply border-accent bg-white dark:bg-gray-900;
}
.drawflow .drawflow-node .input {
@apply bg-accent !important;
}
.drawflow .icon-ui {
position: relative;
}
.drawflow svg:not(.v-remixicon) {
z-index: 0;
position: absolute;
overflow: visible !important
}
.drawflow .connection {
position: absolute;
}
.drawflow .connection .main-path {
fill: none;
stroke-width: 5px;
stroke: theme('colors.accent');
transition: stroke 100ms ease-in-out;
}
.drawflow .connection .main-path:hover {
stroke: theme('colors.yellow.300');
cursor: pointer
}
.drawflow .connection .main-path.selected {
stroke: theme('colors.green.300');
}
.drawflow .connection .point {
cursor: move;
stroke: #000;
stroke-width: 2;
fill: #fff;
}
.drawflow .connection .point.selected,
.drawflow .connection .point:hover {
fill: theme('colors.blue.600');
}
.drawflow .main-path {
fill: none;
stroke-width: 5px;
stroke: theme('colors.accent');
}
.drawflow-node .drawflow-delete {
display: none !important;
}
.drawflow-delete {
position: absolute;
display: block;
width: 20px;
height: 20px;
@apply bg-red-500;
color: #fff;
z-index: 4;
line-height: 20px;
font-weight: 700;
text-align: center;
border-radius: 50%;
font-family: monospace;
cursor: pointer;
text-transform: uppercase;
}
.drawflow>.drawflow-delete {
margin-left: -15px;
margin-top: 15px
}
.parent-node .drawflow-delete {
right: -15px;
top: -15px
}
================================================
FILE: src/assets/css/flow.css
================================================
.vue-flow__minimap {
@apply rounded-lg dark:bg-gray-800;
}
.vue-flow__node {
& > div {
@apply rounded-lg transition;
}
&.selected .block-base__content {
@apply ring-2 ring-accent;
}
&:hover {
.block-menu-container {
display: block;
}
}
&.vue-flow__node-BlockGroup2 {
z-index: 0 !important;
}
.vue-flow__handle {
@apply h-4 w-4 rounded-full border-0;
&.target {
@apply bg-accent -ml-4;
}
&.source {
border-width: 3px;
@apply border-accent -mr-4 bg-white dark:bg-black;
}
}
}
.vue-flow {
&.disabled {
.vue-flow__handle {
pointer-events: none;
}
}
svg g.connected-edges path {
stroke: theme('colors.primary');
}
}
.vue-flow__edge {
cursor: pointer;
&.selected .vue-flow__edge-path {
stroke: theme('colors.green.300');
}
}
.dark .vue-flow__edge-path:hover {
stroke: theme('colors.yellow.400');
}
.vue-flow__edge-path {
stroke: theme('colors.accent');
stroke-width: 4;
transition: stroke 100ms ease;
&:hover {
stroke: theme('colors.yellow.500');
}
}
================================================
FILE: src/assets/css/fonts.css
================================================
@font-face {
font-family: Inter var;
font-weight: 100 900;
font-display: swap;
font-style: normal;
font-named-instance: "Regular";
src: url('../fonts/Inter-roman-latin.var.woff2') format("woff2");
}
/* source-code-pro-regular - latin */
@font-face {
font-family: 'Source Code Pro';
font-style: normal;
font-weight: 400;
src: local(''),
url('../fonts/source-code-pro-v21-latin-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
url('../fonts/source-code-pro-v21-latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* source-code-pro-600 - latin */
@font-face {
font-family: 'Source Code Pro';
font-style: normal;
font-weight: 600;
src: local(''),
url('../fonts/source-code-pro-v21-latin-600.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
url('../fonts/source-code-pro-v21-latin-600.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
================================================
FILE: src/assets/css/style.css
================================================
.list-item-transition {
transition: all 0.4s ease;
}
.list-leave-active {
position: absolute;
width: 100%;
}
.list-enter-from,
.list-leave-to {
opacity: 0;
}
.list-enter-from,
.list-enter-from {
transform: translateY(30px);
}
================================================
FILE: src/assets/css/tailwind.css
================================================
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer utilities {
.hoverable {
@apply hover:bg-gray-800 hover:bg-opacity-5 dark:hover:bg-gray-200 dark:hover:bg-opacity-5;
}
.bg-input {
@apply bg-black bg-opacity-5 hover:bg-opacity-10 dark:bg-gray-200 dark:bg-opacity-5 dark:hover:bg-opacity-10;
}
.bg-box-transparent {
@apply bg-black bg-opacity-5 dark:bg-gray-200 dark:bg-opacity-5;
}
.bg-box-transparent-2 {
@apply bg-black bg-opacity-10 dark:bg-gray-200 dark:bg-opacity-10;
}
}
:host, :root {
--color-primary: 59 130 246;
--color-secondary: 96 165 250;
--color-accent: 24 24 27;
}
.dark {
--color-primary: 96 165 250;
--color-secondary: 59 130 246;
--color-accent: 244 244 245;
}
* {
@apply dark:border-gray-700;
}
html.dark {
@apply bg-gray-900;
}
body, :host {
font-family: 'Inter var', sans-serif !important;
font-size: 16px !important;
font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11';
@apply bg-gray-50 dark:bg-gray-900 leading-normal;
}
table th,
table td {
@apply py-2 px-4;
}
input:focus,
button:focus,
textarea:focus,
select:focus,
[role='button']:focus {
outline: none;
@apply ring-2 ring-accent dark:ring-gray-200;
}
.text-overflow {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.line-clamp {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
.custom-table thead {
@apply bg-box-transparent;
}
.custom-table thead th {
@apply font-semibold;
}
.custom-table thead th:first-child {
@apply rounded-l-lg;
}
.custom-table thead th:last-child {
@apply rounded-r-lg;
}
.custom-table tbody {
@apply divide-y;
}
pre {
font-size: 15px;
}
.scroll,
.scroll .cm-scroller {
&::-webkit-scrollbar {
width: 7px;
height: 9px;
}
&::-webkit-scrollbar-thumb {
@apply bg-gray-300 dark:bg-gray-700;
border-radius: 8px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&.scroll-xs::-webkit-scrollbar {
width: 5px;
height: 5px;
}
}
.tippy-box[data-theme~='tooltip-theme'] {
@apply px-2 py-1 bg-gray-900 dark:bg-gray-200 dark:text-black text-sm text-gray-200 rounded-md;
}
.Vue-Toastification__toast {
font-family: inherit !important;
}
.ProseMirror > * + * {
margin-top: 0.75em;
}
.ProseMirror img {
max-width: 100%;
height: auto;
}
.ProseMirror img.ProseMirror-selectednode {
outline: 3px solid #68CEF8;
}
.input-label {
@apply ml-1 text-sm text-gray-600 dark:text-gray-200;
}
================================================
FILE: src/background/BackgroundEventsListeners.js
================================================
import browser from 'webextension-polyfill';
import { initElementSelector } from '@/newtab/utils/elementSelector';
import dayjs from 'dayjs';
import dbStorage from '@/db/storage';
import cronParser from 'cron-parser';
import BackgroundUtils from './BackgroundUtils';
import BackgroundWorkflowTriggers from './BackgroundWorkflowTriggers';
async function handleScheduleBackup() {
try {
const { localBackupSettings, workflows } = await browser.storage.local.get([
'localBackupSettings',
'workflows',
]);
if (!localBackupSettings) return;
const workflowsData = Object.values(workflows || []).reduce(
(acc, workflow) => {
if (workflow.isProtected) return acc;
delete workflow.$id;
delete workflow.createdAt;
delete workflow.data;
delete workflow.isDisabled;
delete workflow.isProtected;
acc.push(workflow);
return acc;
},
[]
);
const payload = {
workflows: JSON.stringify(workflowsData),
};
if (localBackupSettings.includedItems.includes('storage:table')) {
const tables = await dbStorage.tablesItems.toArray();
payload.storageTables = JSON.stringify(tables);
}
if (localBackupSettings.includedItems.includes('storage:variables')) {
const variables = await dbStorage.variables.toArray();
payload.storageVariables = JSON.stringify(variables);
}
const base64 = btoa(encodeURIComponent(JSON.stringify(payload)));
const filename = `${
localBackupSettings.folderName ? `${localBackupSettings.folderName}/` : ''
}${dayjs().format('DD-MMM-YYYY--HH-mm')}.json`;
await browser.downloads.download({
filename,
url: `data:application/json;base64,${base64}`,
});
await browser.storage.local.set({
localBackupSettings: {
...localBackupSettings,
lastBackup: Date.now(),
},
});
const expression =
localBackupSettings.schedule === 'custom'
? localBackupSettings.customSchedule
: localBackupSettings.schedule;
const parsedExpression = cronParser.parseExpression(expression).next();
if (!parsedExpression) return;
await browser.alarms.create('schedule-local-backup', {
when: parsedExpression.getTime(),
});
} catch (error) {
console.error(error);
}
}
class BackgroundEventsListeners {
static onActionClicked() {
BackgroundUtils.openDashboard();
}
static onCommand(name) {
if (name === 'open-dashboard') {
BackgroundUtils.openDashboard();
} else if (name === 'element-picker') {
initElementSelector();
}
}
static onAlarms(event) {
if (event.name === 'schedule-local-backup') {
handleScheduleBackup();
return;
}
BackgroundWorkflowTriggers.scheduleWorkflow(event);
}
static onWebNavigationCompleted({ tabId, url, frameId }) {
if (frameId > 0) return;
BackgroundWorkflowTriggers.visitWebTriggers(tabId, url);
}
static onContextMenuClicked(event, tab) {
BackgroundWorkflowTriggers.contextMenu(event, tab);
}
static async onNotificationClicked(notificationId) {
if (notificationId.startsWith('logs')) {
const { 1: logId } = notificationId.split(':');
const [tab] = await browser.tabs.query({
url: browser.runtime.getURL('/newtab.html'),
});
if (!tab) await BackgroundUtils.openDashboard('');
await BackgroundUtils.sendMessageToDashboard('open-logs', { logId });
}
}
static onRuntimeStartup() {
browser.storage.local.remove('workflowStates');
(browser.action || browser.browserAction).setBadgeText({ text: '' });
BackgroundWorkflowTriggers.reRegisterTriggers(true);
}
static onHistoryStateUpdated({ frameId, url, tabId }) {
if (frameId !== 0) return;
BackgroundWorkflowTriggers.visitWebTriggers(tabId, url, true);
}
static async onRuntimeInstalled({ reason }) {
try {
if (reason === 'install') {
await browser.storage.local.set({
logs: [],
shortcuts: {},
workflows: [],
collections: [],
workflowState: {},
isFirstTime: true,
visitWebTriggers: [],
});
await browser.windows.create({
type: 'popup',
state: 'maximized',
url: browser.runtime.getURL('newtab.html#/welcome'),
});
return;
}
if (reason === 'update') {
await BackgroundWorkflowTriggers.reRegisterTriggers();
}
} catch (error) {
console.error(error);
}
}
}
export default BackgroundEventsListeners;
================================================
FILE: src/background/BackgroundOffscreen.js
================================================
/* eslint-disable class-methods-use-this */
import { IS_FIREFOX } from '@/common/utils/constant';
import { sleep } from '@/utils/helper';
import { MessageListener } from '@/utils/message';
import Browser from 'webextension-polyfill';
const OFFSCREEN_URL = Browser.runtime.getURL('/offscreen.html');
class BackgroundOffscreen {
/** @type {BackgroundOffscreen} */
static #_instance;
/**
* OffscreenService singleton
* @returns {BackgroundOffscreen}
*/
static get instance() {
if (!this.#_instance) {
this.#_instance = new BackgroundOffscreen();
}
return this.#_instance;
}
/** @type {MessageListener} */
#messageListener;
constructor() {
this.#messageListener = new MessageListener('offscreen');
this.on = this.#messageListener.on;
}
/**
*
* @returns {Promise<boolean>}
*/
async #ensureDocument() {
if (IS_FIREFOX) return;
const isOpened = await this.isOpened();
if (isOpened) return;
await chrome.offscreen.createDocument({
url: OFFSCREEN_URL,
reasons: [
chrome.offscreen.Reason.BLOBS,
chrome.offscreen.Reason.CLIPBOARD,
chrome.offscreen.Reason.IFRAME_SCRIPTING,
],
justification: 'For running the workflow',
});
await sleep(500);
}
/**
*
* @returns {Promise<boolean>}
*/
async isOpened() {
if (IS_FIREFOX) return false;
const contexts = await chrome.runtime.getContexts({
documentUrls: [OFFSCREEN_URL],
contextTypes: ['OFFSCREEN_DOCUMENT'],
});
return Boolean(contexts.length);
}
/**
*
* @param {string} name
* @param {*} data
* @returns {Promise<*>}
*/
async sendMessage(name, data) {
await this.#ensureDocument();
return this.#messageListener.sendMessage(name, data);
}
}
export default BackgroundOffscreen;
================================================
FILE: src/background/BackgroundUtils.js
================================================
import browser from 'webextension-polyfill';
import { waitTabLoaded } from '@/workflowEngine/helper';
class BackgroundUtils {
static async openDashboard(url, updateTab = true) {
const tabUrl = browser.runtime.getURL(
`/newtab.html#${typeof url === 'string' ? url : ''}`
);
try {
const [tab] = await browser.tabs.query({
url: browser.runtime.getURL('/newtab.html'),
});
if (tab) {
const tabOptions = { active: true };
if (updateTab) tabOptions.url = tabUrl;
await browser.tabs.update(tab.id, tabOptions);
if (updateTab) {
await browser.windows.update(tab.windowId, {
focused: true,
state: 'maximized',
});
}
} else {
const curWin = await browser.windows.getCurrent();
const windowOptions = {
top: 0,
left: 0,
width: Math.min(curWin.width, 715),
height: Math.min(curWin.height, 715),
url: tabUrl,
type: 'popup',
};
if (updateTab) {
windowOptions.focused = true;
}
await browser.windows.create(windowOptions);
}
} catch (error) {
console.error(error);
throw error;
}
}
static async sendMessageToDashboard(type, data) {
const [tab] = await browser.tabs.query({
url: browser.runtime.getURL('/newtab.html'),
});
await waitTabLoaded({ tabId: tab.id });
const result = await browser.tabs.sendMessage(tab.id, { type, data });
return result;
}
}
export default BackgroundUtils;
================================================
FILE: src/background/BackgroundWorkflowTriggers.js
================================================
import browser from 'webextension-polyfill';
import dayjs from 'dayjs';
import { findTriggerBlock, parseJSON } from '@/utils/helper';
import {
registerCronJob,
registerSpecificDay,
registerWorkflowTrigger,
} from '@/utils/workflowTrigger';
import BackgroundWorkflowUtils from './BackgroundWorkflowUtils';
class BackgroundWorkflowTriggers {
static async visitWebTriggers(tabId, tabUrl, spa = false) {
const { visitWebTriggers } = await browser.storage.local.get(
'visitWebTriggers'
);
if (!visitWebTriggers || visitWebTriggers.length === 0) return;
const triggeredWorkflow = visitWebTriggers.find(
({ url, isRegex, supportSPA }) => {
if (!url.trim() || (spa && !supportSPA)) return false;
return tabUrl.match(isRegex ? new RegExp(url, 'g') : url);
}
);
if (triggeredWorkflow) {
let workflowId = triggeredWorkflow.id;
if (triggeredWorkflow.id.startsWith('trigger')) {
const { 1: triggerWorkflowId } = triggeredWorkflow.id.split(':');
workflowId = triggerWorkflowId;
}
const workflowData = await BackgroundWorkflowUtils.getWorkflow(
workflowId
);
if (workflowData) {
BackgroundWorkflowUtils.instance.executeWorkflow(workflowData, {
tabId,
});
}
}
}
static async scheduleWorkflow({ name }) {
try {
let workflowId = name;
let triggerId = null;
if (name.startsWith('trigger')) {
const { 1: triggerWorkflowId, 2: triggerItemId } = name.split(':');
triggerId = triggerItemId;
workflowId = triggerWorkflowId;
}
const currentWorkflow = await BackgroundWorkflowUtils.getWorkflow(
workflowId
);
if (!currentWorkflow) return;
let data = currentWorkflow.trigger;
if (!data) {
const drawflow =
typeof currentWorkflow.drawflow === 'string'
? parseJSON(currentWorkflow.drawflow, {})
: currentWorkflow.drawflow;
const { data: triggerBlockData } = findTriggerBlock(drawflow) || {};
data = triggerBlockData;
}
if (triggerId) {
data = data.triggers.find((trigger) => trigger.id === triggerId);
if (data) data = { ...data, ...data.data };
}
if (data && data.type === 'interval' && data.fixedDelay) {
const { workflowStates } = await browser.storage.local.get(
'workflowStates'
);
const workflowState = (workflowStates || []).find(
(item) => item.workflowId === workflowId
);
if (workflowState) {
let { workflowQueue } = await browser.storage.local.get(
'workflowQueue'
);
workflowQueue = workflowQueue || [];
if (!workflowQueue.includes(workflowId)) {
(workflowQueue = workflowQueue || []).push(workflowId);
await browser.storage.local.set({ workflowQueue });
}
return;
}
} else if (data && data.type === 'date') {
const [hour, minute, second] = data.time.split(':');
const date = dayjs(data.date)
.hour(hour)
.minute(minute)
.second(second || 0);
const isAfter = dayjs(Date.now() - 60 * 1000).isAfter(date);
if (isAfter) return;
}
BackgroundWorkflowUtils.instance.executeWorkflow(currentWorkflow);
if (!data) return;
if (['specific-day', 'cron-job'].includes(data.type)) {
if (data.type === 'specific-day') {
registerSpecificDay(name, data);
} else {
registerCronJob(name, data);
}
}
} catch (error) {
console.error(error);
}
}
static async contextMenu({ parentMenuItemId, menuItemId, frameId }, tab) {
try {
if (parentMenuItemId !== 'automaContextMenu') return;
const message = await browser.tabs.sendMessage(
tab.id,
{
type: 'context-element',
},
{ frameId }
);
let workflowId = menuItemId;
if (menuItemId.startsWith('trigger')) {
const { 1: triggerWorkflowId } = menuItemId.split(':');
workflowId = triggerWorkflowId;
}
const workflowData = await BackgroundWorkflowUtils.getWorkflow(
workflowId
);
BackgroundWorkflowUtils.instance.executeWorkflow(workflowData, {
data: {
variables: message,
},
});
} catch (error) {
console.error(error);
}
}
static async reRegisterTriggers(isStartup = false) {
const { workflows, workflowHosts, teamWorkflows } =
await browser.storage.local.get([
'workflows',
'workflowHosts',
'teamWorkflows',
]);
const convertToArr = (value) =>
Array.isArray(value) ? value : Object.values(value);
const workflowsArr = convertToArr(workflows);
if (workflowHosts) {
workflowsArr.push(...convertToArr(workflowHosts));
}
if (teamWorkflows) {
workflowsArr.push(
...BackgroundWorkflowUtils.flattenTeamWorkflows(teamWorkflows)
);
}
for (const currWorkflow of workflowsArr) {
// eslint-disable-next-line no-continue
if (currWorkflow.isDisabled) continue;
let triggerBlock = currWorkflow.trigger;
if (!triggerBlock) {
const flow =
typeof currWorkflow.drawflow === 'string'
? parseJSON(currWorkflow.drawflow, {})
: currWorkflow.drawflow;
triggerBlock = findTriggerBlock(flow)?.data;
}
if (triggerBlock) {
if (isStartup && triggerBlock.type === 'on-startup') {
BackgroundWorkflowUtils.instance.executeWorkflow(currWorkflow);
} else {
if (isStartup && triggerBlock.triggers) {
for (const trigger of triggerBlock.triggers) {
if (trigger.type === 'on-startup') {
await BackgroundWorkflowUtils.executeWorkflow(currWorkflow);
}
}
}
await registerWorkflowTrigger(currWorkflow.id, {
data: triggerBlock,
});
}
}
}
}
}
export default BackgroundWorkflowTriggers;
================================================
FILE: src/background/BackgroundWorkflowUtils.js
================================================
import { IS_FIREFOX } from '@/common/utils/constant';
import browser from 'webextension-polyfill';
import BackgroundOffscreen from './BackgroundOffscreen';
class BackgroundWorkflowUtils {
/** @type {BackgroundWorkflowUtils} */
static #_instance;
/**
* BackgroundWorkflowUtils singleton
* @type {BackgroundWorkflowUtils}
*/
static get instance() {
if (!this.#_instance) this.#_instance = new BackgroundWorkflowUtils();
return this.#_instance;
}
/** @type {import('@/workflowEngine/WorkflowManager').default} */
#workflowManager;
constructor() {
this.#workflowManager = null;
}
static flattenTeamWorkflows(workflows) {
return Object.values(Object.values(workflows || {})[0] || {});
}
static async getWorkflow(workflowId) {
if (!workflowId) return null;
if (workflowId.startsWith('team')) {
const { teamWorkflows } = await browser.storage.local.get(
'teamWorkflows'
);
if (!teamWorkflows) return null;
const workflows = this.flattenTeamWorkflows(teamWorkflows);
return workflows.find((item) => item.id === workflowId);
}
const { workflows, workflowHosts } = await browser.storage.local.get([
'workflows',
'workflowHosts',
]);
let findWorkflow = Array.isArray(workflows)
? workflows.find(({ id }) => id === workflowId)
: workflows[workflowId];
if (!findWorkflow) {
findWorkflow = Object.values(workflowHosts || {}).find(
({ hostId }) => hostId === workflowId
);
if (findWorkflow) findWorkflow.id = findWorkflow.hostId;
}
return findWorkflow;
}
async #ensureWorkflowManager() {
if (!IS_FIREFOX) return;
this.#workflowManager = (
await import('@/workflowEngine/WorkflowManager')
).default.instance;
}
/**
* Stop workflow execution
* @param {string} stateId
* @returns {Promise<void>}
*/
async stopExecution(stateId) {
if (IS_FIREFOX) {
await this.#ensureWorkflowManager();
this.#workflowManager.stopExecution(stateId);
return;
}
await BackgroundOffscreen.instance.sendMessage('workflow:stop', stateId);
}
/**
* Resume workflow execution
* @param {string} stateId
* @param {object} nextBlock
* @returns {Promise<void>}
*/
async resumeExecution(stateId, nextBlock) {
if (IS_FIREFOX) {
await this.#ensureWorkflowManager();
this.#workflowManager.resumeExecution(stateId, nextBlock);
return;
}
await BackgroundOffscreen.instance.sendMessage('workflow:resume', {
id: stateId,
nextBlock,
});
}
/**
* Update workflow execution state
* @param {string} stateId
* @param {object} data
* @returns {Promise<void>}
*/
async updateExecutionState(stateId, data) {
if (IS_FIREFOX) {
await this.#ensureWorkflowManager();
this.#workflowManager.updateExecution(stateId, data);
return;
}
await BackgroundOffscreen.instance.sendMessage('workflow:update', {
data,
id: stateId,
});
}
async executeWorkflow(workflowData, options) {
if (workflowData.isDisabled) return;
if (IS_FIREFOX) {
await this.#ensureWorkflowManager();
this.#workflowManager.execute(workflowData, options);
return;
}
await BackgroundOffscreen.instance.sendMessage('workflow:execute', {
workflow: workflowData,
options,
});
}
}
export default BackgroundWorkflowUtils;
================================================
FILE: src/background/index.js
================================================
import { IS_FIREFOX } from '@/common/utils/constant';
import BrowserAPIEventHandler from '@/service/browser-api/BrowserAPIEventHandler';
import BrowserAPIService from '@/service/browser-api/BrowserAPIService';
import { useUserStore } from '@/stores/user';
import {
executeCallbacksInData,
isCallbackBridge,
} from '@/utils/callbackBridge';
import getFile, { readFileAsBase64 } from '@/utils/getFile';
import { sleep } from '@/utils/helper';
import { MessageListener } from '@/utils/message';
// import { getDocumentCtx } from '@/content/handleSelector';
import { automaRefDataStr } from '@/workflowEngine/helper';
import automa from '@business';
import browser from 'webextension-polyfill';
import { registerWorkflowTrigger } from '../utils/workflowTrigger';
import BackgroundEventsListeners from './BackgroundEventsListeners';
import BackgroundOffscreen from './BackgroundOffscreen';
import BackgroundUtils from './BackgroundUtils';
import BackgroundWorkflowUtils from './BackgroundWorkflowUtils';
BackgroundOffscreen.instance.sendMessage('halo');
browser.alarms.onAlarm.addListener(BackgroundEventsListeners.onAlarms);
browser.commands.onCommand.addListener(BackgroundEventsListeners.onCommand);
(browser.action || browser.browserAction).onClicked.addListener(
BackgroundEventsListeners.onActionClicked
);
browser.runtime.onStartup.addListener(
BackgroundEventsListeners.onRuntimeStartup
);
browser.runtime.onInstalled.addListener(
BackgroundEventsListeners.onRuntimeInstalled
);
browser.webNavigation.onCompleted.addListener(
BackgroundEventsListeners.onWebNavigationCompleted
);
browser.webNavigation.onHistoryStateUpdated.addListener(
BackgroundEventsListeners.onHistoryStateUpdated
);
const contextMenu = IS_FIREFOX ? browser.menus : browser.contextMenus;
if (contextMenu && contextMenu.onClicked) {
contextMenu.onClicked.addListener(
BackgroundEventsListeners.onContextMenuClicked
);
}
if (browser.notifications && browser.notifications.onClicked) {
browser.notifications.onClicked.addListener(
BackgroundEventsListeners.onNotificationClicked
);
}
const message = new MessageListener('background');
message.on('browser-api', (payload) => {
return BrowserAPIService.runtimeMessageHandler.call(
BrowserAPIService,
payload
);
});
message.on(BrowserAPIEventHandler.RuntimeEvents.TOGGLE, (data) =>
BrowserAPIEventHandler.instance.onToggleBrowserEventListener(data)
);
message.on('fetch', async ({ type, resource }) => {
const response = await fetch(resource.url, resource);
if (!response.ok) throw new Error(response.statusText);
let result = null;
if (type === 'base64') {
const blob = await response.blob();
const base64 = await readFileAsBase64(blob);
result = base64;
} else {
result = await response[type]();
}
return result;
});
message.on('fetch:text', (url) => {
return fetch(url).then((response) => response.text());
});
message.on('open:dashboard', (url) => BackgroundUtils.openDashboard(url));
message.on('set:active-tab', (tabId) => {
return browser.tabs.update(tabId, { active: true });
});
message.on('debugger:send-command', ({ tabId, method, params }) => {
return new Promise((resolve) => {
chrome.debugger.sendCommand({ tabId }, method, params, resolve);
});
});
message.on('debugger:type', ({ tabId, commands, delay }) => {
return new Promise((resolve) => {
let index = 0;
async function executeCommands() {
const command = commands[index];
if (!command) {
resolve();
return;
}
chrome.debugger.sendCommand(
{ tabId },
'Input.dispatchKeyEvent',
command,
async () => {
if (delay > 0) await sleep(delay);
index += 1;
executeCommands();
}
);
}
executeCommands();
});
});
message.on('get:sender', (_, sender) => sender);
message.on('get:file', (path) => getFile(path));
message.on('get:tab-screenshot', (options, sender) =>
browser.tabs.captureVisibleTab(sender.tab.windowId, options)
);
message.on('dashboard:refresh-packages', async () => {
const tabs = await browser.tabs.query({
url: browser.runtime.getURL('/newtab.html'),
});
tabs.forEach((tab) => {
browser.tabs.sendMessage(tab.id, {
type: 'refresh-packages',
});
});
});
message.on('workflow:stop', (stateId) =>
BackgroundWorkflowUtils.instance.stopExecution(stateId)
);
message.on('workflow:execute', async (workflowData, sender) => {
if (workflowData.includeTabId) {
if (!workflowData.options) workflowData.options = {};
workflowData.options.tabId = sender.tab.id;
}
BackgroundWorkflowUtils.instance.executeWorkflow(
workflowData,
workflowData?.options || {}
);
});
message.on(
'workflow:added',
({ workflowId, teamId, workflowData, source = 'community' }) => {
let path = `/workflows/${workflowId}`;
if (source === 'team') {
if (!teamId) return;
path = `/teams/${teamId}/workflows/${workflowId}`;
}
browser.tabs
.query({ url: browser.runtime.getURL('/newtab.html') })
.then((tabs) => {
if (tabs.length >= 1) {
const lastTab = tabs.at(-1);
tabs.forEach((tab) => {
browser.tabs.sendMessage(tab.id, {
data: { workflowId, teamId, source, workflowData },
type: 'workflow:added',
});
});
browser.tabs.update(lastTab.id, {
active: true,
});
browser.windows.update(lastTab.windowId, { focused: true });
} else {
BackgroundUtils.openDashboard(`${path}?permission=true`);
}
});
}
);
message.on('workflow:register', ({ triggerBlock, workflowId }) => {
registerWorkflowTrigger(workflowId, triggerBlock);
});
message.on('recording:stop', async () => {
try {
await BackgroundUtils.openDashboard('', false);
await BackgroundUtils.sendMessageToDashboard('recording:stop');
} catch (error) {
console.error(error);
}
});
message.on('workflow:resume', ({ id, nextBlock }) => {
if (!id) return;
BackgroundWorkflowUtils.instance.resumeExecution(id, nextBlock);
});
message.on('workflow:breakpoint', (id) => {
if (!id) return;
BackgroundWorkflowUtils.instance.updateExecutionState(id, {
status: 'breakpoint',
});
});
message.on('get:user-id', async () => {
const userStore = useUserStore();
return { userId: userStore.user?.id };
});
message.on(
'check-csp-and-inject',
async ({ target, debugMode, callback, options, injectOptions }) => {
try {
const [isBlockedByCSP] = await browser.scripting.executeScript({
target,
// eslint-disable-next-line object-shorthand
func: function () {
return new Promise((resolve) => {
const escapePolicy = (script) => {
if (window?.trustedTypes?.createPolicy) {
try {
// 生成基于白名单的唯一策略名称,避免与现有策略冲突
const baseNames = [
'default',
'dompurify',
'jSecure',
'forceInner',
];
let escapeElPolicy = null;
// 为每个基础名称尝试添加唯一后缀
for (const baseName of baseNames) {
try {
const uniqueName = `${baseName}-automa-${Date.now().toString(
36
)}`;
escapeElPolicy = window.trustedTypes.createPolicy(
uniqueName,
{
createHTML: (to_escape) => to_escape,
createScript: (to_escape) => to_escape,
}
);
// 如果成功创建,跳出循环
break;
} catch (e) {
// 该名称失败(可能不在白名单中),继续尝试下一个
}
}
// 如果基于白名单的策略都失败,尝试纯 automa 策略名
if (!escapeElPolicy) {
try {
const automaName = `automa-policy-${Date.now().toString(
36
)}`;
escapeElPolicy = window.trustedTypes.createPolicy(
automaName,
{
createHTML: (to_escape) => to_escape,
createScript: (to_escape) => to_escape,
}
);
} catch (e) {
// 最后的尝试也失败了
}
}
// 如果成功创建了策略,使用它
if (escapeElPolicy) {
return escapeElPolicy.createScript(script);
}
// 如果所有策略名称都失败,返回原始脚本
return script;
} catch (e) {
// 捕获任何其他错误并降级
return script;
}
}
return script;
};
const eventListener = ({ srcElement }) => {
if (!srcElement || srcElement.id !== 'automa-csp') return;
srcElement.remove();
resolve(true);
};
document.addEventListener('securitypolicyviolation', eventListener);
const script = document.createElement('script');
script.id = 'automa-csp';
script.innerText = escapePolicy('console.log("...")');
setTimeout(() => {
document.removeEventListener(
'securitypolicyviolation',
eventListener
);
resolve(false);
}, 500);
document.body.appendChild(script);
});
},
world: 'MAIN',
...injectOptions,
});
// CSP blocked
if (isBlockedByCSP.result) {
await new Promise((resolve) => {
chrome.debugger.attach({ tabId: target.tabId }, '1.3', resolve);
});
// 首先执行回调函数以获取JS代码字符串
// 这里的关键是回调函数本身就是一个字符串,直接在debugger中执行
const callbackString =
typeof callback === 'function' ? callback.toString() : callback;
if (!callbackString) {
throw new Error('Callback is missing or invalid');
}
// 直接执行回调函数的字符串表示,不再通过额外的包装函数
const wrappedCallback = `
(function() {
try {
const fn = ${callbackString};
return fn();
} catch (err) {
console.error("Error in callback execution:", err);
return JSON.stringify({ error: err.message });
}
})()
`;
// 执行回调函数以获取JavaScript代码
const jsCodeResult = await chrome.debugger.sendCommand(
{ tabId: target.tabId },
'Runtime.evaluate',
{
expression: wrappedCallback,
userGesture: true,
returnByValue: true,
...options,
}
);
if (!jsCodeResult || !jsCodeResult.result) {
console.error('无法获取JavaScript代码,结果为空');
throw new Error('Unable to get JavaScript code');
}
if (
jsCodeResult.result.subtype === 'error' ||
jsCodeResult.exceptionDetails
) {
console.error(
'执行回调函数时出错:',
jsCodeResult.result.description || jsCodeResult.exceptionDetails
);
throw new Error(
jsCodeResult.result.description || 'Error executing callback'
);
}
// 确保我们获取到的是字符串类型的JavaScript代码
if (typeof jsCodeResult.result.value !== 'string') {
console.error('回调函数返回的不是JavaScript代码字符串');
throw new Error('Callback did not return JavaScript code string');
}
const jsCode = jsCodeResult.result.value;
// 执行生成的JavaScript代码
const execResult = await chrome.debugger.sendCommand(
{ tabId: target.tabId },
'Runtime.evaluate',
{
expression: jsCode,
userGesture: true,
awaitPromise: true,
returnByValue: true,
...options,
}
);
if (!debugMode) {
await chrome.debugger.detach({ tabId: target.tabId });
}
if (!execResult || !execResult.result) {
console.error('无法执行代码,结果为空');
throw new Error('Unable execute code');
}
if (
execResult.result.subtype === 'error' ||
execResult.exceptionDetails
) {
console.error(
'执行JavaScript代码时出错:',
execResult.result.description || execResult.exceptionDetails
);
throw new Error(
execResult.result.description || 'Error executing JavaScript code'
);
}
return {
isBlocked: true,
value: execResult.result.value || null,
};
}
return { isBlocked: false };
} catch (error) {
console.error(error);
return { isBlocked: false, error: error.message };
}
}
);
const getAutomaScript = ({ varName, refData, everyNewTab, isEval = false }) => {
let str = `
const ${varName} = ${JSON.stringify(refData)};
${automaRefDataStr(varName)}
function automaSetVariable(name, value) {
const variables = ${varName}.variables;
if (!variables) ${varName}.variables = {}
${varName}.variables[name] = value;
}
function automaNextBlock(data, insert = true) {
if (${isEval}) {
Promise.resolve({
columns: {
data,
insert,
},
variables: ${varName}.variables,
});
} else{
document.body.dispatchEvent(new CustomEvent('__automa-next-block__', { detail: { data, insert, refData: ${varName} } }));
}
}
function automaResetTimeout() {
if (${isEval}) {
clearTimeout($automaTimeout);
$automaTimeout = setTimeout(() => {
resolve();
}, $automaTimeoutMs);
} else {
document.body.dispatchEvent(new CustomEvent('__automa-reset-timeout__'));
}
}
function automaFetchClient(id, { type, resource }) {
return new Promise((resolve, reject) => {
const validType = ['text', 'json', 'base64'];
if (!type || !validType.includes(type)) {
reject(new Error('The "type" must be "text" or "json"'));
return;
}
const eventName = \`__automa-fetch-response-\${id}__\`;
const eventListener = ({ detail }) => {
if (detail.id !== id) return;
window.removeEventListener(eventName, eventListener);
if (detail.isError) {
reject(new Error(detail.result));
} else {
resolve(detail.result);
}
};
window.addEventListener(eventName, eventListener);
window.dispatchEvent(
new CustomEvent(\`__automa-fetch__\`, {
detail: {
id,
type,
resource,
},
})
);
});
}
function automaFetch(type, resource) {
return automaFetchClient('${varName}', { type, resource });
}
`;
if (everyNewTab) str = automaRefDataStr(varName);
return str;
};
message.on(
'script:execute',
async ({ target, blockData, varName, preloadScripts }) => {
try {
const automaScript = getAutomaScript({
varName,
isEval: false,
refData: blockData.refData,
everyNewTab: blockData.data.everyNewTab,
});
const result = await browser.scripting.executeScript({
target,
func: ($blockData, $preloadScripts, $automaScript) => {
return new Promise((resolve, reject) => {
try {
const $documentCtx = document;
// fixme: 需要处理iframe的情况
// if ($blockData.frameSelector) {
// const iframeCtx = getDocumentCtx($blockData.frameSelector);
// if (!iframeCtx) {
// reject(new Error('iframe-not-found'));
// return;
// }
// $documentCtx = iframeCtx;
// }
const scriptAttr = `block--${$blockData.id}`;
const isScriptExists = $documentCtx.querySelector(
`.automa-custom-js[${scriptAttr}]`
);
if (isScriptExists) {
resolve('');
return;
}
const script = $documentCtx.createElement('script');
script.setAttribute(scriptAttr, '');
script.classList.add('automa-custom-js');
script.textContent = `
(() => {
// Setup context
${$automaScript}
// Execute user code
try {
${$blockData.data.code}
${
$blockData.data.everyNewTab ||
$blockData.data.code.includes('automaNextBlock')
? ''
: 'automaNextBlock()'
}
} catch (error) {
console.error(error);
${
$blockData.data.everyNewTab
? ''
: 'automaNextBlock({ $error: true, message: error.message })'
}
}
})();
`;
const preloadScriptsEl = $preloadScripts.map((item) => {
const scriptEl = $documentCtx.createElement('script');
scriptEl.id = item.id;
scriptEl.textContent = item.script;
return {
element: scriptEl,
removeAfterExec: item.removeAfterExec,
};
});
if (!$blockData.data.everyNewTab) {
let timeout;
let onNextBlock;
let onResetTimeout;
const cleanUp = () => {
script.remove();
preloadScriptsEl.forEach((item) => {
if (item.removeAfterExec) item.element.remove();
});
clearTimeout(timeout);
$documentCtx.body.removeEventListener(
'__automa-reset-timeout__',
onResetTimeout
);
$documentCtx.body.removeEventListener(
'__automa-next-block__',
onNextBlock
);
};
onNextBlock = ({ detail }) => {
cleanUp();
if (!detail) {
resolve({ columns: {}, variables: {} });
return;
}
const payload = {
insert: detail.insert,
data: detail.data?.$error
? detail.data
: JSON.stringify(detail?.data ?? {}),
};
resolve({
columns: payload,
variables: detail.refData?.variables,
});
};
onResetTimeout = () => {
clearTimeout(timeout);
timeout = setTimeout(cleanUp, $blockData.data.timeout);
};
$documentCtx.body.addEventListener(
'__automa-next-block__',
onNextBlock
);
$documentCtx.body.addEventListener(
'__automa-reset-timeout__',
onResetTimeout
);
timeout = setTimeout(cleanUp, $blockData.data.timeout);
} else {
resolve();
}
// Inject scripts in the correct order
preloadScriptsEl.forEach((item) => {
$documentCtx.head.appendChild(item.element);
});
$documentCtx.head.appendChild(script);
} catch (error) {
console.error('javascriptBlockUtil error', error);
reject(error);
}
});
},
world: 'MAIN',
args: [blockData, preloadScripts, automaScript],
});
return [{ result: result[0].result }];
} catch (err) {
return { result: null, msg: err.message, error: err };
}
}
);
message.on('script:execute-callback', async ({ target, callback }) => {
try {
// 首先尝试使用scripting API执行脚本
const result = await browser.scripting.executeScript({
target,
func: ($callbackFn) => {
try {
const script = document.createElement('script');
script.textContent = `
(() => {
${$callbackFn}
})()
`;
document.body.appendChild(script);
return { success: true };
} catch (error) {
console.error('执行脚本时出错:', error);
return { success: false, error: error.message };
}
},
world: 'MAIN',
args: [callback],
});
// 检查执行结果
const executionResult = result[0]?.result;
if (executionResult && executionResult.success) {
return true;
}
// 如果常规方法失败,尝试使用debugger API
await new Promise((resolve) => {
chrome.debugger.attach({ tabId: target.tabId }, '1.3', resolve);
});
// 使用debugger API执行脚本
const execResult = await chrome.debugger.sendCommand(
{ tabId: target.tabId },
'Runtime.evaluate',
{
expression: `(() => { ${callback} })()`,
userGesture: true,
awaitPromise: false,
returnByValue: true,
}
);
// 执行完成后分离debugger
await chrome.debugger.detach({ tabId: target.tabId });
if (!execResult || !execResult.result) {
console.error('使用debugger API执行脚本失败');
return false;
}
return true;
} catch (error) {
console.error('执行script:execute-callback时出错:', error);
return false;
}
});
const DOWNLOADS_STORAGE_KEY = 'automa-rename-downloaded-files';
const getFileExtension = (str) => /(?:\.([^.]+))?$/.exec(str)[1];
const downloadListeners = {
registered: false,
changedCallbacks: new Map(),
pendingRequests: [],
downloadDataCache: new Map(),
handledFilenameCallbacks: new Set(),
suggestCalled: new Set(),
downloadInfo: new Map(),
};
/**
* @param {Object} item
* @param {Function} suggest
* @returns {boolean}
*/
function determineFilenameListener(item, suggest) {
const downloadKey = `download-${item.id}`;
if (downloadListeners.suggestCalled.has(downloadKey)) {
return true;
}
downloadListeners.suggestCalled.add(downloadKey);
setTimeout(async () => {
try {
let suggestion = null;
if (downloadListeners.downloadDataCache.has(item.id)) {
suggestion = downloadListeners.downloadDataCache.get(item.id);
} else {
const result = await browser.storage.session.get(DOWNLOADS_STORAGE_KEY);
const filesData = result[DOWNLOADS_STORAGE_KEY] || {};
suggestion = filesData[item.id];
}
if (!suggestion) {
// we should not call suggest again, because Chrome expects us to handle it
return;
}
if (!suggestion.filename || suggestion.filename.trim() === '') {
return;
}
const hasFileExt = getFileExtension(suggestion.filename);
if (!hasFileExt) {
const fileExtension = getFileExtension(item.filename);
suggestion.filename += `.${fileExtension}`;
}
let conflictAction = 'uniquify';
const validActions = ['uniquify', 'overwrite', 'prompt'];
if (
suggestion.onConflict &&
validActions.includes(suggestion.onConflict)
) {
conflictAction = suggestion.onConflict;
}
if (!suggestion.waitForDownload) {
downloadListeners.downloadDataCache.delete(item.id);
const result = await browser.storage.session.get(DOWNLOADS_STORAGE_KEY);
const filesData = result[DOWNLOADS_STORAGE_KEY] || {};
delete filesData[item.id];
await browser.storage.session.set({
[DOWNLOADS_STORAGE_KEY]: filesData,
});
}
downloadListeners.handledFilenameCallbacks.add(downloadKey);
try {
suggest({
filename: suggestion.filename,
conflictAction,
});
} catch (callbackError) {
console.error('❌ failed to call suggest callback:', callbackError);
}
} catch (error) {
console.error('❌ failed to handle download filename:', error);
}
}, 0);
// important: we use async processing, so we must return true
return true;
}
function handleDownloadChanged(downloadDelta) {
const { id, state, filename } = downloadDelta;
if (!id || !downloadListeners.changedCallbacks.has(id)) return;
if (!downloadListeners.downloadInfo.has(id)) {
downloadListeners.downloadInfo.set(id, {
downloadId: id,
state: null,
filename: null,
});
}
const downloadInfo = downloadListeners.downloadInfo.get(id);
if (state) {
downloadInfo.state = state.current;
}
if (filename) {
downloadInfo.filename = filename.current;
}
if (
downloadInfo.state &&
['complete', 'interrupted'].includes(downloadInfo.state)
) {
const callback = downloadListeners.changedCallbacks.get(id);
const completeInfo = {
...downloadInfo,
filename:
downloadInfo.filename ||
(downloadListeners.downloadDataCache.has(id)
? downloadListeners.downloadDataCache.get(id).filename
: null),
};
try {
callback(completeInfo);
} catch (callbackError) {
console.error(
'❌ failed to call download changed callback:',
callbackError
);
}
downloadListeners.changedCallbacks.delete(id);
downloadListeners.downloadDataCache.delete(id);
downloadListeners.downloadInfo.delete(id);
const downloadKey = `download-${id}`;
downloadListeners.handledFilenameCallbacks.delete(downloadKey);
downloadListeners.suggestCalled.delete(downloadKey);
}
}
async function handleDownloadCreated(downloadItem) {
try {
let isHandled = false;
const pendingDownloads = downloadListeners.pendingRequests || [];
if (pendingDownloads.length > 0) {
const pendingRequest = pendingDownloads.shift();
const { downloadData, callback } = pendingRequest;
// save to memory cache immediately to avoid race condition
downloadListeners.downloadDataCache.set(downloadItem.id, downloadData);
// save to storage
const result = await browser.storage.session.get(DOWNLOADS_STORAGE_KEY);
const filesData = result[DOWNLOADS_STORAGE_KEY] || {};
filesData[downloadItem.id] = downloadData;
await browser.storage.session.set({ [DOWNLOADS_STORAGE_KEY]: filesData });
if (downloadData.waitForDownload && callback) {
downloadListeners.changedCallbacks.set(downloadItem.id, callback);
}
isHandled = true;
}
if (!isHandled) {
const result = await browser.storage.session.get(DOWNLOADS_STORAGE_KEY);
const filesData = result[DOWNLOADS_STORAGE_KEY] || {};
if (filesData[downloadItem.id]) {
downloadListeners.downloadDataCache.set(
downloadItem.id,
filesData[downloadItem.id]
);
}
}
} catch (error) {
console.error('❌ failed to handle download created:', error);
}
}
function cleanupDownloadListeners() {
const MAX_AGE = 60 * 60 * 1000; // 1 hour
const now = Date.now();
if (downloadListeners.handledFilenameCallbacksTimestamp) {
if (now - downloadListeners.handledFilenameCallbacksTimestamp > MAX_AGE) {
downloadListeners.handledFilenameCallbacks.clear();
}
}
downloadListeners.handledFilenameCallbacksTimestamp = now;
}
setInterval(cleanupDownloadListeners, 60 * 60 * 1000);
async function registerBackgroundDownloadListeners() {
try {
if (browser.downloads.onCreated.hasListener(handleDownloadCreated)) {
browser.downloads.onCreated.removeListener(handleDownloadCreated);
}
if (
!IS_FIREFOX &&
browser.downloads.onDeterminingFilename &&
browser.downloads.onDeterminingFilename.hasListener(
determineFilenameListener
)
) {
browser.downloads.onDeterminingFilename.removeListener(
determineFilenameListener
);
}
if (browser.downloads.onChanged.hasListener(handleDownloadChanged)) {
browser.downloads.onChanged.removeListener(handleDownloadChanged);
}
downloadListeners.handledFilenameCallbacks.clear();
downloadListeners.suggestCalled.clear();
downloadListeners.downloadInfo.clear();
if (downloadListeners.registered) {
downloadListeners.registered = false;
}
} catch (cleanupError) {
console.warn('⚠️ failed to cleanup existing listeners:', cleanupError);
}
if (downloadListeners.registered) return;
try {
// 确保有下载权限
const hasPermission = await browser.permissions.contains({
permissions: ['downloads'],
});
if (!hasPermission) {
console.error('❌ no download permission, cannot register listeners');
return;
}
browser.downloads.onCreated.addListener(handleDownloadCreated);
if (!IS_FIREFOX && browser.downloads.onDeterminingFilename) {
browser.downloads.onDeterminingFilename.addListener(
determineFilenameListener
);
}
browser.downloads.onChanged.addListener(handleDownloadChanged);
downloadListeners.registered = true;
downloadListeners.pendingRequests = [];
downloadListeners.handledFilenameCallbacksTimestamp = Date.now();
} catch (error) {
console.error('❌ failed to register download listeners:', error);
}
}
message.on('downloads:register-listeners', async () => {
await registerBackgroundDownloadListeners();
return true;
});
message.on('downloads:watch-created', async (data) => {
await registerBackgroundDownloadListeners();
// save pending download requests
downloadListeners.pendingRequests = downloadListeners.pendingRequests || [];
// Handle callback bridge or regular function
let safeCallback = null;
if (data.onComplete) {
if (isCallbackBridge(data.onComplete)) {
// Use callback bridge for cross-context communication
safeCallback = async (response) => {
try {
await executeCallbacksInData(data.onComplete, response);
} catch (callbackError) {
console.error(
'❌ failed to call download complete callback:',
callbackError
);
}
};
} else if (typeof data.onComplete === 'function') {
// Fallback for regular functions (should not happen in fixed version)
safeCallback = (response) => {
try {
data.onComplete(response);
} catch (callbackError) {
console.error(
'❌ failed to call download complete callback:',
callbackError
);
}
};
}
}
downloadListeners.pendingRequests.push({
downloadData: data.downloadData,
tabId: data.tabId,
callback: safeCallback,
});
return true;
});
message.on('downloads:watch-changed', async ({ downloadId, onComplete }) => {
await registerBackgroundDownloadListeners();
if (downloadId && onComplete) {
let safeCallback = null;
if (isCallbackBridge(onComplete)) {
// Use callback bridge for cross-context communication
safeCallback = async (response) => {
try {
await executeCallbacksInData(onComplete, response);
} catch (callbackError) {
console.error(
'❌ failed to call download changed callback:',
callbackError
);
}
};
} else if (typeof onComplete === 'function') {
// Fallback for regular functions (should not happen in fixed version)
safeCallback = (response) => {
try {
onComplete(response);
} catch (callbackError) {
console.error(
'❌ failed to call download changed callback:',
callbackError
);
}
};
}
if (safeCallback) {
downloadListeners.changedCallbacks.set(downloadId, safeCallback);
}
}
return true;
});
automa('background', message);
browser.runtime.onMessage.addListener(message.listener);
================================================
FILE: src/common/utils/constant.js
================================================
export const IS_FIREFOX = BROWSER_TYPE === 'firefox';
================================================
FILE: src/components/block/BlockBase.vue
================================================
<template>
<div
class="block-base relative w-48"
:data-block-id="blockId"
@dblclick.stop="$emit('edit')"
>
<div
class="block-menu-container absolute top-0 hidden w-full"
style="transform: translateY(-100%)"
>
<div class="pointer-events-none">
<p
title="Block id (click to copy)"
class="block-menu pointer-events-auto text-overflow inline-block px-1 dark:text-gray-300"
style="max-width: 96px; margin-bottom: 0"
@click="insertToClipboard"
>
{{ isCopied ? '✅ Copied' : blockId }}
</p>
</div>
<div class="block-menu inline-flex items-center dark:text-gray-300">
<button
v-if="!blockData.details?.disableDelete"
title="Delete block"
@click.stop="$emit('delete')"
>
<v-remixicon size="20" name="riDeleteBin7Line" />
</button>
<button
:title="$t('workflow.blocks.base.settings.title')"
@click.stop="
$emit('settings', { details: blockData.details, data, blockId })
"
>
<v-remixicon size="20" name="riSettings3Line" />
</button>
<button
v-if="!excludeGroupBlocks.includes(blockData.details?.id)"
:title="$t('workflow.blocks.base.moveToGroup')"
draggable="true"
class="cursor-move"
@dragstart="handleStartDrag"
@mousedown.stop
>
<v-remixicon name="riDragDropLine" size="20" />
</button>
<button
v-if="blockData.details?.id !== 'trigger'"
title="Enable/Disable block"
@click.stop="$emit('update', { disableBlock: !data.disableBlock })"
>
<v-remixicon
size="20"
:name="data.disableBlock ? 'riToggleLine' : 'riToggleFill'"
/>
</button>
<button title="Run workflow from here" @click.stop="runWorkflow">
<v-remixicon size="20" name="riPlayLine" />
</button>
<button
v-if="!blockData.details?.disableEdit"
title="Edit block"
@click="$emit('edit')"
>
<v-remixicon size="20" name="riPencilLine" />
</button>
<slot name="action" />
</div>
</div>
<slot name="prepend" />
<ui-card :class="contentClass" class="block-base__content relative z-10">
<v-remixicon
v-if="workflow?.data?.value.testingMode"
:class="{ 'text-red-500 dark:text-red-400': data.$breakpoint }"
class="absolute left-0 top-0"
name="riRecordCircleFill"
title="Set as breakpoint"
size="20"
@click="$emit('update', { $breakpoint: !data.$breakpoint })"
/>
<slot></slot>
</ui-card>
<slot name="append" />
</div>
</template>
<script setup>
import { excludeGroupBlocks } from '@/utils/shared';
import { inject, ref } from 'vue';
const props = defineProps({
contentClass: {
type: String,
default: '',
},
blockData: {
type: Object,
default: () => ({}),
},
data: {
type: Object,
default: () => ({}),
},
blockId: {
type: String,
default: '',
},
});
defineEmits(['delete', 'edit', 'update', 'settings']);
const isCopied = ref(false);
const workflow = inject('workflow', null);
const workflowUtils = inject('workflow-utils', null);
function insertToClipboard() {
navigator.clipboard.writeText(props.blockId);
isCopied.value = true;
setTimeout(() => {
isCopied.value = false;
}, 1000);
}
function handleStartDrag(event) {
const payload = {
data: props.data,
fromBlockBasic: true,
blockId: props.blockId,
id: props.blockData.details.id,
};
event.dataTransfer.setData('block', JSON.stringify(payload));
}
function runWorkflow() {
if (!workflowUtils) return;
workflowUtils.executeFromBlock(props.blockId);
}
</script>
<style>
.block-menu {
@apply mb-1 bg-box-transparent-2 rounded-md;
button {
padding-left: 6px;
padding-right: 6px;
@apply focus:ring-0 py-1 hover:text-primary;
}
}
</style>
================================================
FILE: src/components/block/BlockBasic.vue
================================================
<template>
<block-base
:id="componentId"
:data="data"
:block-id="id"
:block-data="block"
:data-position="JSON.stringify(position)"
class="block-basic group"
@edit="$emit('edit')"
@delete="$emit('delete', id)"
@update="$emit('update', $event)"
@settings="$emit('settings', $event)"
>
<Handle
v-if="label !== 'trigger'"
:id="`${id}-input-1`"
type="target"
:position="Position.Left"
/>
<div class="flex items-center">
<span
:class="data.disableBlock ? 'bg-box-transparent' : block.category.color"
class="mr-2 inline-block rounded-lg p-2 dark:text-black"
>
<svg
v-if="block.details.name === 'AI Workflow'"
width="31.2"
height="31.2"
viewBox="0 0 14 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5.22626 4.28601V8.14343H1.36884V4.28601H5.22626Z"
stroke="black"
/>
<path
d="M12.6076 0.50061V3.64319H9.46503V0.50061H12.6076Z"
stroke="black"
/>
<path
d="M12.6309 8.35657V11.4991H9.48834V8.35657H12.6309Z"
stroke="black"
/>
<path d="M5.66516 6.37384H7.27247V2.13159H9.45839" stroke="black" />
<path d="M5.15082 6.43445H7.26688V9.9986H9.91184" stroke="black" />
</svg>
<v-remixicon
v-else
:path="getIconPath(block.details.icon)"
:name="block.details.icon || 'riGlobalLine'"
/>
</span>
<div class="flex-1 overflow-hidden">
<span
v-if="blockErrors"
v-tooltip="{
allowHTML: true,
content: blockErrors,
}"
class="absolute top-2 right-2 text-red-500 dark:text-red-400"
>
<v-remixicon name="riAlertLine" size="20" />
</span>
<p
v-if="block.details.id"
class="text-overflow whitespace-nowrap font-semibold leading-tight"
>
{{ getBlockName() }}
</p>
<p
:class="{ 'mb-1': data.description && data.loopId }"
class="text-overflow leading-tight text-gray-600 dark:text-gray-200"
>
{{ data.description }}
</p>
<span
v-if="showTextToCopy"
:title="showTextToCopy.name + ' (click to copy)'"
class="bg-box-transparent text-overflow absolute bottom-0 right-0 rounded-sm rounded-br-lg py-px px-1 text-xs text-gray-600 dark:text-gray-200"
style="max-width: 40%; cursor: pointer"
@click.stop="insertToClipboard(showTextToCopy.value)"
>
{{ state.isCopied ? '✅ Copied' : showTextToCopy.value }}
</span>
</div>
</div>
<slot :block="block"></slot>
<div
v-if="data.onError?.enable && data.onError?.toDo === 'fallback'"
class="fallback flex items-center justify-end"
>
<v-remixicon
v-if="block"
:title="t('workflow.blocks.base.onError.fallbackTitle')"
name="riInformationLine"
size="18"
/>
<span class="ml-1">
{{ t('common.fallback') }}
</span>
</div>
<Handle :id="`${id}-output-1`" type="source" :position="Position.Right" />
<Handle
v-if="data.onError?.enable && data.onError?.toDo === 'fallback'"
:id="`${id}-output-fallback`"
type="source"
:position="Position.Right"
style="top: auto; bottom: 10px"
/>
</block-base>
</template>
<script setup>
import { useBlockValidation } from '@/composable/blockValidation';
import { useComponentId } from '@/composable/componentId';
import { useEditorBlock } from '@/composable/editorBlock';
import { Handle, Position } from '@vue-flow/core';
import { computed, shallowReactive } from 'vue';
import { useI18n } from 'vue-i18n';
import BlockBase from './BlockBase.vue';
const props = defineProps({
id: {
type: String,
default: '',
},
label: {
type: String,
default: '',
},
data: {
type: Object,
default: () => ({}),
},
position: {
type: Object,
default: () => ({}),
},
events: {
type: Object,
default: () => ({}),
},
dimensions: {
type: Object,
default: () => ({}),
},
});
defineEmits(['delete', 'edit', 'update', 'settings']);
const loopBlocks = ['loop-data', 'loop-elements'];
const { t, te } = useI18n();
const block = useEditorBlock(props.label);
const componentId = useComponentId('block-base');
const { errors: blockErrors } = useBlockValidation(
props.label,
() => props.data
);
const state = shallowReactive({
isCopied: false,
});
const showTextToCopy = computed(() => {
if (loopBlocks.includes(block.details.id) && props.data.loopId) {
return {
name: 'Loop id',
value: props.data.loopId,
};
}
if (block.details.id === 'google-sheets' && props.data.refKey) {
return {
name: 'Reference key',
value: props.data.refKey,
};
}
return null;
});
function insertToClipboard(text) {
navigator.clipboard.writeText(text);
state.isCopied = true;
setTimeout(() => {
state.isCopied = false;
}, 1000);
}
function getBlockName() {
const key = `workflow.blocks.${block.details.id}.name`;
return te(key) ? t(key) : block.details.name;
}
function getIconPath(path) {
if (path && path.startsWith('path')) {
const { 1: iconPath } = path.split(':');
return iconPath;
}
return '';
}
</script>
================================================
FILE: src/components/block/BlockBasicWithFallback.vue
================================================
<template>
<block-base
:id="componentId"
:data="data"
:block-id="id"
:block-data="block"
class="block-basic group"
@edit="$emit('edit')"
@delete="$emit('delete', id)"
@update="$emit('update', $event)"
@settings="$emit('settings', $event)"
>
<Handle :id="`${id}-input-1`" type="target" :position="Position.Left" />
<div class="flex items-center">
<span
:class="data.disableBlock ? 'bg-box-transparent' : block.category.color"
class="mr-2 inline-block rounded-lg p-2 dark:text-black"
>
<v-remixicon :name="block.details.icon || 'riGlobalLine'" />
</span>
<div class="flex-1 overflow-hidden">
<p
v-if="block.details.id"
class="text-overflow whitespace-nowrap font-semibold leading-tight"
>
{{ t(`workflow.blocks.${block.details.id}.name`) }}
</p>
<p class="text-overflow leading-tight text-gray-600 dark:text-gray-200">
{{ data.description }}
</p>
</div>
</div>
<span
v-if="blockErrors"
v-tooltip="{
allowHTML: true,
content: blockErrors,
}"
class="absolute top-2 right-2 text-red-500 dark:text-red-400"
>
<v-remixicon name="riAlertLine" size="20" />
</span>
<slot :block="block"></slot>
<div class="fallback flex items-center justify-end">
<v-remixicon
v-if="block"
:title="t('workflow.blocks.base.onError.fallbackTitle')"
name="riInformationLine"
size="18"
/>
<span class="ml-1">
{{ t('common.fallback') }}
</span>
</div>
<Handle :id="`${id}-output-1`" type="source" :position="Position.Right" />
<Handle
:id="`${id}-output-fallback`"
type="source"
:position="Position.Right"
style="top: auto; bottom: 10px"
/>
</block-base>
</template>
<script setup>
import { Handle, Position } from '@vue-flow/core';
import { useI18n } from 'vue-i18n';
import { useBlockValidation } from '@/composable/blockValidation';
import { useEditorBlock } from '@/composable/editorBlock';
import { useComponentId } from '@/composable/componentId';
import BlockBase from './BlockBase.vue';
const props = defineProps({
id: {
type: String,
default: '',
},
label: {
type: String,
default: '',
},
data: {
type: Object,
default: () => ({}),
},
});
defineEmits(['delete', 'edit', 'update', 'settings']);
const { t } = useI18n();
const block = useEditorBlock(props.label);
const componentId = useComponentId('block-base');
const { errors: blockErrors } = useBlockValidation(
props.label,
() => props.data
);
</script>
================================================
FILE: src/components/block/BlockConditions.vue
================================================
<template>
<block-base
:id="componentId"
:data="data"
:block-id="id"
:block-data="block"
class="w-64"
@edit="$emit('edit')"
@delete="$emit('delete', id)"
@update="$emit('update', $event)"
@settings="$emit('settings', $event)"
>
<Handle :id="`${id}-input-1`" type="target" :position="Position.Left" />
<div class="flex items-center">
<div
:class="data.disableBlock ? 'bg-box-transparent' : block.category.color"
class="mr-4 inline-block rounded-lg p-2 text-sm dark:text-black"
>
<v-remixicon name="riAB" size="20" class="mr-1 inline-block" />
<span>{{ t('workflow.blocks.conditions.name') }}</span>
</div>
</div>
<p
v-show="data.description"
class="text-overflow mt-2 leading-tight text-gray-600 dark:text-gray-200"
>
{{ data.description }}
</p>
<ul
v-if="data.conditions && data.conditions.length !== 0"
class="mt-4 space-y-2"
>
<li
v-for="item in data.conditions"
:key="item.id"
class="bg-box-transparent relative flex w-full flex-1 items-center rounded-lg p-2"
@dblclick.stop="$emit('edit', { editCondition: item.id })"
>
<p
v-if="item.name"
class="text-overflow w-full text-right"
:title="item.name"
>
{{ item.name }}
</p>
<template v-else>
<p class="text-overflow w-5/12 text-right">
{{ item.compareValue || '_____' }}
</p>
<p class="mx-1 w-2/12 text-center font-mono">
{{ item.type }}
</p>
<p class="text-overflow w-5/12">
{{ item.value || '_____' }}
</p>
</template>
<Handle
:id="`${id}-output-${item.id}`"
:position="Position.Right"
style="margin-right: -33px"
type="source"
/>
</li>
<p
v-if="data.conditions && data.conditions.length !== 0"
class="text-right text-gray-600 dark:text-gray-200"
>
<span title="Fallback"> ⓘ </span>
Fallback
</p>
</ul>
<Handle
v-if="data.conditions.length > 0"
:id="`${id}-output-fallback`"
:position="Position.Right"
type="source"
style="top: auto; bottom: 10px"
/>
</block-base>
</template>
<script setup>
import { useI18n } from 'vue-i18n';
import { Handle, Position } from '@vue-flow/core';
import { useComponentId } from '@/composable/componentId';
import { useEditorBlock } from '@/composable/editorBlock';
import BlockBase from './BlockBase.vue';
const props = defineProps({
id: {
type: String,
default: '',
},
label: {
type: String,
default: '',
},
data: {
type: Object,
default: () => ({}),
},
});
defineEmits(['delete', 'settings', 'edit', 'update']);
const { t } = useI18n();
const componentId = useComponentId('block-conditions');
const block = useEditorBlock(props.label);
</script>
<style>
.condition-handle {
position: relative !important;
top: 82px !important;
margin-bottom: 32px !important;
}
</style>
================================================
FILE: src/components/block/BlockDelay.vue
================================================
<template>
<block-base
:id="componentId"
:data="data"
:block-id="id"
:block-data="block"
class="w-48"
@delete="$emit('delete', id)"
@update="$emit('update', $event)"
@settings="$emit('settings', $event)"
>
<Handle :id="`${id}-input-1`" type="target" :position="Position.Left" />
<div class="mb-2 flex items-center">
<div
:class="data.disableBlock ? 'bg-box-transparent' : block.category.color"
class="mr-4 inline-block rounded-lg p-2 text-sm dark:text-black"
>
<v-remixicon name="riTimerLine" size="20" class="mr-1 inline-block" />
<span>{{ t('workflow.blocks.delay.name') }}</span>
</div>
<div class="grow"></div>
<v-remixicon
name="riDeleteBin7Line"
class="cursor-pointer"
@click.stop="$emit('delete', id)"
/>
</div>
<input
:value="data.time"
min="0"
:title="t('workflow.blocks.delay.input.title')"
:placeholder="t('workflow.blocks.delay.input.placeholder')"
class="bg-input w-full rounded-lg px-4 py-2"
type="text"
required
@keydown.stop
@input="$emit('update', { time: $event.target.value })"
/>
<div
v-if="block.details.id !== 'trigger'"
:title="t('workflow.blocks.base.moveToGroup')"
draggable="true"
class="move-to-group invisible absolute -top-2 -right-2 z-50 rounded-md bg-white p-1 shadow-md dark:bg-gray-700"
@dragstart="handleStartDrag"
@mousedown.stop
>
<v-remixicon name="riDragDropLine" size="20" />
</div>
<Handle :id="`${id}-output-1`" type="source" :position="Position.Right" />
</block-base>
</template>
<script setup>
import { useI18n } from 'vue-i18n';
import { Handle, Position } from '@vue-flow/core';
import { useComponentId } from '@/composable/componentId';
import { useEditorBlock } from '@/composable/editorBlock';
import BlockBase from './BlockBase.vue';
const props = defineProps({
id: {
type: String,
default: '',
},
label: {
type: String,
default: '',
},
data: {
type: Object,
default: () => ({}),
},
});
defineEmits(['update', 'delete', 'settings']);
const { t } = useI18n();
const block = useEditorBlock(props.label);
const componentId = useComponentId('block-delay');
function handleStartDrag(event) {
const payload = {
id: props.label,
data: props.data,
blockId: props.id,
fromBlockBasic: true,
};
event.dataTransfer.setData('block', JSON.stringify(payload));
}
</script>
================================================
FILE: src/components/block/BlockElementExists.vue
================================================
<template>
<block-base
:id="componentId"
:data="data"
:block-id="id"
:block-data="block"
style="width: 195px"
@edit="$emit('edit')"
@delete="$emit('delete', id)"
@update="$emit('update', $event)"
@settings="$emit('settings', $event)"
>
<Handle :id="`${id}-input-1`" type="target" :position="Position.Left" />
<div
:class="data.disableBlock ? 'bg-box-transparent' : block.category.color"
class="mb-2 inline-block rounded-lg p-2 text-sm dark:text-black"
>
<v-remixicon name="riFocus3Line" size="20" class="mr-1 inline-block" />
<span>{{ t('workflow.blocks.element-exists.name') }}</span>
</div>
<p
:title="t('workflow.blocks.element-exists.selector')"
:class="{ 'font-mono': !data.description }"
class="text-overflow bg-box-transparent mb-2 rounded-lg p-2 text-right text-sm"
style="max-width: 200px"
>
{{
data.description ||
data.selector ||
t('workflow.blocks.element-exists.selector')
}}
</p>
<p class="text-right text-gray-600 dark:text-gray-200">
<span :title="t('workflow.blocks.element-exists.fallbackTitle')">
ⓘ
</span>
{{ t('common.fallback') }}
</p>
<Handle :id="`${id}-output-1`" type="source" :position="Position.Right" />
<Handle
:id="`${id}-output-2`"
type="source"
:position="Position.Right"
style="top: auto; bottom: 12px"
/>
</block-base>
</template>
<script setup>
import { useI18n } from 'vue-i18n';
import { Handle, Position } from '@vue-flow/core';
import { useComponentId } from '@/composable/componentId';
import { useEditorBlock } from '@/composable/editorBlock';
import BlockBase from './BlockBase.vue';
const props = defineProps({
id: {
type: String,
default: '',
},
label: {
type: String,
default: '',
},
data: {
type: Object,
default: () => ({}),
},
});
defineEmits(['delete', 'edit', 'update', 'settings']);
const { t } = useI18n();
const block = useEditorBlock(props.label);
const componentId = useComponentId('block-delay');
</script>
<style>
.drawflow .element-exists .outputs {
top: 70px !important;
transform: none !important;
}
.drawflow .element-exists .output {
margin-bottom: 22px;
}
</style>
================================================
FILE: src/components/block/BlockGroup.vue
================================================
<template>
<block-base
:id="componentId"
:data="data"
:block-id="id"
:block-data="block"
class="w-64"
content-class="!p-0"
@edit="$emit('edit')"
@delete="$emit('delete', id)"
@update="$emit('update', $event)"
@settings="$emit('settings', $event)"
>
<Handle :id="`${id}-input-1`" type="target" :position="Position.Left" />
<div class="p-4">
<div class="mb-2 flex items-center">
<div
:class="
data.disableBlock ? 'bg-box-transparent' : block.category.color
"
class="mr-4 inline-flex items-center rounded-lg p-2 text-sm dark:text-black"
>
<v-remixicon
:name="block.details.icon || 'riFolderZipLine'"
size="20"
class="mr-2 inline-block"
/>
<span>{{ t('workflow.blocks.blocks-group.name') }}</span>
</div>
</div>
<input
:value="data.name"
:placeholder="t('workflow.blocks.blocks-group.groupName')"
type="text"
class="w-full bg-transparent focus:ring-0"
@keydown.stop
@input="$emit('update', { name: $event.target.value })"
/>
</div>
<draggable
:model-value="blocks"
item-key="itemId"
class="nowheel scroll max-h-60 space-y-1 overflow-auto px-4 pb-4 text-sm"
@mousedown.stop
@dragover.prevent
@drop="handleDrop"
@update:modelValue="$emit('update', { blocks: $event })"
>
<template #item="{ element, index }">
<div
class="bg-input group flex items-center space-x-2 rounded-lg p-2"
style="cursor: grab"
:data-block-id="element.id"
@dragstart="onDragStart(element, $event)"
@dragend="onDragEnd(element.itemId)"
>
<v-remixicon
:name="tasks[element.id].icon"
size="20"
class="shrink-0"
/>
<div class="flex-1 overflow-hidden leading-tight">
<p class="text-overflow">
{{
getTranslation(
`workflow.blocks.${element.id}.name`,
tasks[element.id].name
)
}}
</p>
<p
:title="element.data.description"
class="text-overflow text-gray-600 dark:text-gray-200"
>
{{ element.data.description }}
</p>
</div>
<div class="invisible group-hover:visible">
<v-remixicon
v-if="workflow?.data?.value.testingMode"
:class="{
'text-red-500 dark:text-red-400': element.data.$breakpoint,
}"
title="Set as breakpoint"
name="riRecordCircleLine"
size="18"
class="mr-2 inline-block cursor-pointer"
@click="toggleBreakpoint(element, index)"
/>
<v-remixicon
name="riPencilLine"
size="18"
class="mr-2 inline-block cursor-pointer"
@click="editBlock(element)"
/>
<v-remixicon
name="riSettings3Line"
size="18"
class="mr-2 inline-block cursor-pointer"
@click="editItemSettings(element)"
/>
<v-remixicon
name="riDeleteBin7Line"
size="18"
class="inline-block cursor-pointer"
@click="deleteItem(index, element.itemId)"
/>
</div>
</div>
</template>
<template #footer>
<div
class="rounded-lg border border-dashed p-2 text-center text-gray-600 dark:text-gray-200"
>
{{ t('workflow.blocks.blocks-group.dropText') }}
</div>
</template>
</draggable>
<Handle :id="`${id}-output-1`" type="source" :position="Position.Right" />
</block-base>
</template>
<script setup>
import { inject, computed, shallowReactive } from 'vue';
import { useI18n } from 'vue-i18n';
import { nanoid } from 'nanoid';
import { useToast } from 'vue-toastification';
import { Handle, Position } from '@vue-flow/core';
import draggable from 'vuedraggable';
import { tasks, excludeGroupBlocks } from '@/utils/shared';
import { useComponentId } from '@/composable/componentId';
import { useEditorBlock } from '@/composable/editorBlock';
import BlockBase from './BlockBase.vue';
const props = defineProps({
id: {
type: String,
default: '',
},
label: {
type: String,
default: '',
},
data: {
type: Object,
default: () => ({}),
},
editor: {
type: Object,
default: () => ({}),
},
events: {
type: Object,
default: () => ({}),
},
});
const emit = defineEmits(['update', 'delete', 'edit', 'settings']);
const { t, te } = useI18n();
const toast = useToast();
const componentId = useComponentId('blocks-group');
const block = useEditorBlock(props.label);
const workflow = inject('workflow', {});
const blocks = computed(() =>
Array.isArray(props.data.blocks)
? props.data.blocks
: Object.values(props.data.blocks)
);
function editItemSettings(element) {
emit('settings', {
blockId: props.id,
data: element.data,
itemId: element.itemId,
details: { id: element.id },
});
}
function onDragStart(item, event) {
event.dataTransfer.setData(
'block',
JSON.stringify({ ...tasks[item.id], ...item, fromGroup: true })
);
}
function onDragEnd(itemId) {
setTimeout(() => {
const blockEl = document.querySelector(`[group-item-id="${itemId}"]`);
if (blockEl) {
const blockIndex = blocks.value.findIndex(
(item) => item.itemId === itemId
);
if (blockIndex !== -1) {
const copyBlocks = [...props.data.blocks];
copyBlocks.splice(blockIndex, 1);
emit('update', { blocks: copyBlocks });
}
}
}, 200);
}
function editBlock(payload) {
emit('edit', payload);
}
function deleteItem(index, itemId) {
const copyBlocks = [...props.data.blocks];
if (workflow.editState.blockData.itemId === itemId) {
workflow.editState.editing = false;
workflow.editState.blockData = false;
}
copyBlocks.splice(index, 1);
emit('update', { blocks: copyBlocks });
}
function getTranslation(key, defText = '') {
return te(key) ? t(key) : defText;
}
function handleDrop(event) {
event.preventDefault();
event.stopPropagation();
const droppedBlock = JSON.parse(event.dataTransfer.getData('block') || null);
if (!droppedBlock || droppedBlock.fromGroup) return;
const { id, data, blockId } = droppedBlock;
if (excludeGroupBlocks.includes(id)) {
toast.error(
t('workflow.blocks.blocks-group.cantAdd', {
blockName: t(`workflow.blocks.${id}.name`),
})
);
return;
}
if (blockId) {
emit('delete', blockId);
}
const copyBlocks = [
...props.data.blocks,
shallowReactive({ id, data, itemId: nanoid(5) }),
];
emit('update', { blocks: copyBlocks });
}
function toggleBreakpoint(item, index) {
const copyBlocks = [...props.data.blocks];
copyBlocks[index].data = {
...copyBlocks[index].data,
$breakpoint: !item.data.$breakpoint,
};
emit('update', { blocks: copyBlocks });
}
</script>
================================================
FILE: src/components/block/BlockGroup2.vue
================================================
<template>
<div
:style="{
width: `${data.width || 400}px`,
height: `${data.height || 300}px`,
}"
class="group-block-2 group relative rounded-lg border-2"
style="
min-width: 400px;
min-height: 300px;
border-color: #2563eb;
background-color: rgb(37, 99, 235, 0.3);
"
>
<div class="flex items-center p-4">
<input
:value="data.name"
placeholder="name"
type="text"
class="rounded-lg bg-white px-4 py-2"
@input="emit('update', { name: $event.target.value })"
/>
<div class="flex-1" />
<v-remixicon
name="riDeleteBin7Line"
class="cursor-pointer"
@click="$emit('delete', id)"
/>
</div>
<span
ref="dragHandle"
style="cursor: nw-resize"
class="drag-handle invisible absolute bottom-0 right-0 h-4 w-4 bg-accent group-hover:visible"
/>
</div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
defineProps({
id: {
type: String,
default: '',
},
label: {
type: String,
default: '',
},
data: {
type: Object,
default: () => ({}),
},
position: {
type: Object,
default: () => ({}),
},
events: {
type: Object,
default: () => ({}),
},
dimensions: {
type: Object,
default: () => ({}),
},
});
const emit = defineEmits(['delete', 'edit', 'update']);
let parent = null;
const initialRect = {
x: 0,
y: 0,
width: 0,
height: 0,
};
const dragHandle = ref(null);
function onMousemove(event) {
event.preventDefault();
event.stopPropagation();
const width = initialRect.width + event.clientX - initialRect.x;
const height = initialRect.height + event.clientY - initialRect.y;
parent.style.width = `${width}px`;
parent.style.height = `${height}px`;
emit('update', { height, width });
}
function onMouseup() {
document.documentElement.removeEventListener('mouseup', onMouseup);
document.documentElement.removeEventListener('mousemove', onMousemove);
}
function initDragging(event) {
event.preventDefault();
event.stopPropagation();
const { height, width } = getComputedStyle(parent);
initialRect.x = event.clientX;
initialRect.y = event.clientY;
initialRect.width = parseInt(width, 10);
initialRect.height = parseInt(height, 10);
document.documentElement.addEventListener('mouseup', onMouseup);
document.documentElement.addEventListener('mousemove', onMousemove);
}
onMounted(() => {
parent = dragHandle.value.closest('.group-block-2');
dragHandle.value.addEventListener('mousedown', initDragging);
});
onBeforeUnmount(() => {
dragHandle.value.removeEventListener('mousedown', initDragging);
});
</script>
================================================
FILE: src/components/block/BlockLoopBreakpoint.vue
================================================
<template>
<block-base
:id="componentId"
:data="data"
:block-id="id"
:block-data="block"
class="w-48"
@delete="$emit('delete', id)"
@update="$emit('update', $event)"
@settings="$emit('settings', $event)"
>
<Handle :id="`${id}-input-1`" type="target" :position="Position.Left" />
<div class="mb-2 flex items-center">
<div
:class="data.disableBlock ? 'bg-box-transparent' : block.category.color"
class="text-overflow mr-4 inline-block rounded-lg p-2 text-sm dark:text-black"
>
<v-remixicon name="riStopLine" size="20" class="mr-1 inline-block" />
<span>{{ t('workflow.blocks.loop-breakpoint.name') }}</span>
</div>
<div class="grow"></div>
<v-remixicon
name="riDeleteBin7Line"
class="cursor-pointer"
@click.stop="$emit('delete', id)"
/>
</div>
<input
:value="data.loopId"
class="bg-input w-full rounded-lg px-4 py-2"
placeholder="Loop ID"
type="text"
required
@keydown.stop
@input="handleInput"
/>
<ui-checkbox
:model-value="data.clearLoop"
class="mt-2"
@change="$emit('update', { clearLoop: $event })"
>
Stop loop
</ui-checkbox>
<Handle :id="`${id}-output-1`" type="source" :position="Position.Right" />
</block-base>
</template>
<script setup>
import { useI18n } from 'vue-i18n';
import { Handle, Position } from '@vue-flow/core';
import { useComponentId } from '@/composable/componentId';
import { useEditorBlock } from '@/composable/editorBlock';
import BlockBase from './BlockBase.vue';
const props = defineProps({
id: {
type: String,
default: '',
},
label: {
type: String,
default: '',
},
data: {
type: Object,
default: () => ({}),
},
});
const emit = defineEmits(['delete', 'update', 'settings']);
const { t } = useI18n();
const block = useEditorBlock(props.label);
const componentId = useComponentId('block-delay');
function handleInput({ target }) {
const loopId = target.value.replace(/\s/g, '');
emit('update', { loopId });
}
</script>
================================================
FILE: src/components/block/BlockNote.vue
================================================
<template>
<div
:class="[data.color || 'white', colors[data.color || 'white']]"
class="block-note rounded-lg p-4"
style="min-width: 192px"
>
<div class="flex items-center border-b pb-2">
<v-remixicon name="riFileEditLine" size="20" />
<p class="mx-2 flex-1 font-semibold">Note</p>
<ui-popover class="note-color">
<template #trigger>
<v-remixicon
name="riSettings3Line"
size="20"
class="cursor-pointer"
/>
</template>
<p class="mb-1 ml-1 text-sm text-gray-600 dark:text-gray-200">Colors</p>
<div class="flex items-center space-x-2">
<span
v-for="(color, colorId) in colors"
:key="colorId"
:class="color"
style="border-width: 3px"
class="inline-block h-8 w-8 cursor-pointer rounded-full"
@click="updateData({ color: colorId })"
/>
</div>
<ui-select
:model-value="data.fontSize"
label="Font size"
class="mt-2 w-full"
@change="updateData({ fontSize: $event })"
>
<option
v-for="(size, fontId) in fontSize"
:key="fontId"
:value="fontId"
>
{{ size.name }}
</option>
</ui-select>
</ui-popover>
<hr class="mx-2 h-7 border-r" />
<v-remixicon
name="riDeleteBin7Line"
size="20"
class="cursor-pointer"
@click="$emit('delete', id)"
/>
</div>
<textarea
:value="data.note"
:style="initialSize"
:class="[fontSize[data.fontSize || 'regular'].class]"
placeholder="Write a note here..."
cols="30"
rows="7"
style="resize: both; min-width: 280px; min-height: 168px"
class="mt-2 bg-transparent focus:ring-0"
@keydown.stop
@input="updateData({ note: $event.target.value })"
@mousedown.stop
@mouseup="onMouseup"
/>
</div>
</template>
<script setup>
import { debounce } from '@/utils/helper';
const props = defineProps({
id: {
type: String,
default: '',
},
label: {
type: String,
default: '',
},
data: {
type: Object,
default: () => ({}),
},
});
const emit = defineEmits(['update', 'delete']);
const initialSize = {
width: `${props.data.width}px`,
height: `${props.data.height}px`,
};
const colors = {
white: 'bg-white dark:bg-gray-800',
red: 'bg-red-200 dark:bg-red-300',
indigo: 'bg-indigo-200 dark:bg-indigo-300',
green: 'bg-green-200 dark:bg-green-300',
amber: 'bg-amber-200 dark:bg-amber-300',
sky: 'bg-sky-200 dark:bg-sky-300',
};
const fontSize = {
regular: {
name: 'Regular',
class: 'text-base',
},
medium: {
name: 'Medium',
class: 'text-xl',
},
large: {
name: 'Large',
class: 'text-2xl',
},
'extra-large': {
name: 'Extra Large',
class: 'text-3xl',
},
};
const updateData = debounce((data) => {
emit('update', data);
}, 250);
function onMouseup({ target }) {
let { height, width } = target.style;
width = parseInt(width, 10);
height = parseInt(height, 10);
if (width === props.data.width && height === props.data.height) return;
updateData({ height, width });
}
</script>
<style>
.note-color .ui-popover__trigger {
@apply flex items-center;
}
.block-note * {
border-color: rgb(0 0 0 / 12%);
}
.dark .block-note {
&:not(.white) {
@apply text-gray-900;
}
&.white * {
border-color: rgb(255 255 255 / 12%);
}
* {
border-color: rgb(0 0 0 / 12%);
}
}
</style>
================================================
FILE: src/components/block/BlockPackage.vue
================================================
<template>
<block-base
:id="componentId"
:data="data"
:block-id="id"
:block-data="block"
class="block-package w-64"
@delete="$emit('delete', id)"
@update="$emit('update', $event)"
@settings="$emit('settings', $event)"
>
<div class="flex items-center">
<img
v-if="data.icon.startsWith('http')"
:src="data.icon"
width="36"
height="36"
class="mr-2 rounded-lg"
/>
<div
:class="data.disableBlock ? 'bg-box-transparent' : block.category.color"
class="mr-4 inline-block overflow-hidden rounded-lg p-2 text-sm dark:text-black"
>
<v-remixicon
v-if="!data.icon.startsWith('http')"
:name="data.icon"
size="20"
class="mr-1 inline-block"
/>
<span class="text-overflow">{{ data.name || 'Unnamed package' }}</span>
</div>
<div class="grow" />
<v-remixicon
v-if="state.isInstalled"
title="Update package"
name="riRefreshLine"
class="cursor-pointer"
@click="updatePackage"
/>
<v-remixicon
v-else
title="Install package"
name="riDownloadLine"
class="cursor-pointer"
@click="installPackage"
/>
</div>
<div class="mt-4 grid grid-cols-2 gap-x-2">
<ul class="pkg-handle-container">
<li
v-for="input in data.inputs"
:key="input.id"
:title="input.name"
class="target relative"
>
<Handle
:id="`${id}-input-${input.id}`"
type="target"
:position="Position.Left"
/>
<p class="text-overflow">{{ input.name }}</p>
</li>
</ul>
<ul class="pkg-handle-container">
<li
v-for="output in data.outputs"
:key="output.id"
:title="output.name"
class="source relative"
>
<Handle
:id="`${id}-output-${output.id}`"
type="source"
:position="Position.Right"
/>
<p class="text-overflow">{{ output.name }}</p>
</li>
</ul>
</div>
<div
v-if="data.author"
class="mt-1 flex items-center text-sm text-gray-600 dark:text-gray-200"
>
<p>By {{ data.author }}</p>
<a
:href="`https://extension.automa.site/packages/${data.id}`"
target="_blank"
title="Open package page"
class="ml-2"
>
<v-remixicon size="18" name="riExternalLinkLine" />
</a>
</div>
</block-base>
</template>
<script setup>
import { useComponentId } from '@/composable/componentId';
import { useEditorBlock } from '@/composable/editorBlock';
import { usePackageStore } from '@/stores/package';
import { Handle, Position } from '@vue-flow/core';
import cloneDeep from 'lodash.clonedeep';
import { onMounted, shallowReactive } from 'vue';
import BlockBase from './BlockBase.vue';
const props = defineProps({
id: {
type: String,
default: '',
},
label: {
type: String,
default: '',
},
data: {
type: Object,
default: () => ({}),
},
editor: {
type: Object,
default: null,
},
});
const emit = defineEmits(['update', 'delete', 'settings']);
const packageStore = usePackageStore();
const block = useEditorBlock(props.label);
const componentId = useComponentId('block-package');
const state = shallowReactive({
isInstalled: false,
});
function installPackage() {
packageStore
.insert({ ...props.data, isExternal: Boolean(props.data.author) }, false)
.then(() => {
state.isInstalled = true;
});
}
function removeConnections(type, old, newEdges) {
const removedEdges = [];
old.forEach((edge) => {
const isNotDeleted = newEdges.find((item) => item.id === edge.id);
if (isNotDeleted) return;
const handleType = type.slice(0, -1);
removedEdges.push(`${props.id}-${handleType}-${edge.id}`);
});
const edgesToRemove = props.editor.getEdges.value.filter(
({ sourceHandle, targetHandle }) => {
if (type === 'outputs') {
return removedEdges.includes(sourceHandle);
}
return removedEdges.includes(targetHandle);
}
);
props.editor.removeEdges(edgesToRemove);
}
function updatePackage() {
const pkg = packageStore.getById(props.data.id);
if (!pkg) return;
const currentInputs = [...props.data.inputs];
const currentOutputs = [...props.data.outputs];
removeConnections('inputs', currentInputs, pkg.inputs);
removeConnections('outputs', currentOutputs, pkg.outputs);
emit('update', cloneDeep(pkg));
}
onMounted(() => {
state.isInstalled = packageStore.getById(props.data.id);
});
</script>
<style>
.pkg-handle-container li {
@apply h-8 flex items-center text-sm;
&.target .vue-flow__handle {
margin-left: -33px;
}
&.source {
@apply justify-end;
.vue-flow__handle {
margin-right: -33px;
}
}
}
</style>
================================================
FILE: src/components/block/BlockRepeatTask.vue
================================================
<template>
<block-base
:id="componentId"
:data="data"
:block-id="id"
:block-data="block"
class="repeat-task w-64"
@delete="$emit('delete', id)"
@update="$emit('update', $event)"
@settings="$emit('settings', $event)"
>
<Handle :id="`${id}-input-1`" type="target" :position="Position.Left" />
<div class="mb-2 flex items-center">
<div
:class="data.disableBlock ? 'bg-box-transparent' : block.category.color"
class="mr-4 inline-block rounded-lg p-2 text-sm dark:text-black"
>
<v-remixicon name="riRepeat2Line" size="20" class="mr-1 inline-block" />
<span>{{ t('workflow.blocks.repeat-task.name') }}</span>
</div>
</div>
<div class="bg-input relative flex items-center rounded-lg">
<input
:value="data.repeatFor"
placeholder="0"
class="bg-transparent py-2 px-4 focus:ring-0"
style="padding-right: 57px; width: 95%"
@keydown.stop
@input="handleInput"
/>
<span class="absolute right-4 text-gray-600 dark:text-gray-200">
{{ t('workflow.blocks.repeat-task.times') }}
</span>
</div>
<p class="text-right text-gray-600 dark:text-gray-200">
{{ t('workflow.blocks.repeat-task.repeatFrom') }}
</p>
<Handle :id="`${id}-output-1`" type="source" :position="Position.Right" />
<Handle
:id="`${id}-output-2`"
type="source"
:position="Position.Right"
style="top: auto; bottom: 12px"
/>
</block-base>
</template>
<script setup>
import { useI18n } from 'vue-i18n';
import { Handle, Position } from '@vue-flow/core';
import { useComponentId } from '@/composable/componentId';
import { useEditorBlock } from '@/composable/editorBlock';
import BlockBase from './BlockBase.vue';
const { t } = useI18n();
const props = defineProps({
id: {
type: String,
default: '',
},
label: {
type: String,
default: '',
},
data: {
type: Object,
default: () => ({}),
},
});
const emit = defineEmits(['delete', 'update', 'settings']);
const block = useEditorBlock(props.label);
const componentId = useComponentId('block-delay');
function handleInput({ target }) {
emit('update', { repeatFor: target.value });
}
</script>
<style>
.drawflow .repeat-task .outputs {
top: 74px !important;
transform: none !important;
}
.drawflow .repeat-task .output {
margin-bottom: 22px;
}
</style>
================================================
FILE: src/components/content/selector/SelectorBlocks.vue
================================================
<template>
<div class="events mt-4">
<div class="flex items-center">
<ui-select
v-model="state.selectedBlock"
class="mr-4 flex-1 p-0.5"
placeholder="Select block"
@change="onSelectChanged"
>
<option v-for="(block, id) in blocks" :key="id" :value="id">
{{ block.name }}
</option>
</ui-select>
<ui-button
:disabled="!state.selectedBlock"
variant="accent"
@click="executeBlock"
>
Execute
</ui-button>
</div>
<component
:is="blocks[state.selectedBlock].component"
v-if="state.selectedBlock && blocks[state.selectedBlock].component"
:data="state.params"
:hide-base="true"
@update:data="updateParams"
/>
<pre
v-if="state.blockResult"
class="mt-2 h-full overflow-auto rounded-lg bg-accent p-2 text-sm text-gray-100"
>{{ state.blockResult }}</pre
>
</div>
</template>
<script setup>
import { shallowReactive } from 'vue';
import { tasks } from '@/utils/shared';
import EditForms from '@/components/newtab/workflow/edit/EditForms.vue';
import EditTriggerEvent from '@/components/newtab/workflow/edit/EditTriggerEvent.vue';
import EditScrollElement from '@/components/newtab/workflow/edit/EditScrollElement.vue';
import handleForms from '@/content/blocksHandler/handlerForms';
import handleGetText from '@/content/blocksHandler/handlerGetText';
import handleEventClick from '@/content/blocksHandler/handlerEventClick';
import handelTriggerEvent from '@/content/blocksHandler/handlerTriggerEvent';
import handleElementScroll from '@/content/blocksHandler/handlerElementScroll';
const props = defineProps({
selector: {
type: String,
default: '',
},
elements: {
type: Array,
default: () => [],
},
});
const emit = defineEmits(['update', 'execute']);
const blocks = {
forms: {
...tasks.forms,
component: EditForms,
handler: handleForms,
},
'get-text': {
...tasks['get-text'],
component: '',
handler: handleGetText,
},
'event-click': {
...tasks['event-click'],
component: '',
handler: handleEventClick,
},
'trigger-event': {
...tasks['trigger-event'],
component: EditTriggerEvent,
handler: handelTriggerEvent,
},
'element-scroll': {
...tasks['element-scroll'],
component: EditScrollElement,
handler: handleElementScroll,
},
};
const state = shallowReactive({
params: {},
blockResult: '',
selectedBlock: '',
});
function updateParams(data = {}) {
state.params = data;
emit('update');
}
function onSelectChanged(value) {
state.params = tasks[value].data;
state.blockResult = '';
emit('update');
}
function executeBlock() {
const params = {
...state.params,
selector: props.selector,
multiple: props.elements.length > 1,
};
emit('execute', true);
blocks[state.selectedBlock].handler({ data: params }).then((result) => {
state.blockResult = JSON.stringify(result, null, 2).trim();
emit('update');
emit('execute', false);
});
}
</script>
================================================
FILE: src/components/content/selector/SelectorElementList.vue
================================================
<template>
<ul class="mt-2 space-y-4">
<li
v-for="(element, index) in elements"
:key="index"
@mouseenter="$emit('highlight', { highlight: true, index, element })"
@mouseleave="$emit('highlight', { highlight: false, index, element })"
>
<p class="mb-1">#{{ index + 1 }} {{ elementName }}</p>
<slot name="item" v-bind="{ element }" />
</li>
</ul>
</template>
<script setup>
defineProps({
elements: {
type: Array,
default: () => [],
},
elementName: {
type: String,
default: 'Element',
},
});
defineEmits(['highlight']);
</script>
================================================
FILE: src/components/content/selector/SelectorElementsDetail.vue
================================================
<template>
<ui-tabs
v-if="!hideBlocks || selectElements.length > 0"
:model-value="activeTab"
class="mt-2"
fill
@change="$emit('update:activeTab', $event)"
>
<ui-tab value="attributes"> Attributes </ui-tab>
<ui-tab v-if="selectElements.length > 0" value="options"> Options </ui-tab>
<ui-tab v-if="!hideBlocks" value="blocks"> Blocks </ui-tab>
</ui-tabs>
<ui-tab-panels
:model-value="activeTab"
class="scroll overflow-y-auto"
style="max-height: calc(100vh - 17rem)"
>
<ui-tab-panel value="attributes">
<selector-element-list
:elements="selectedElements"
@highlight="$emit('highlight', $event)"
>
<template #item="{ element }">
<div
v-for="(value, name) in element.attributes"
:key="name"
class="bg-box-transparent mb-1 rounded-lg py-2 px-3"
>
<p
class="text-overflow text-sm leading-tight text-gray-600"
title="Attribute name"
>
{{ name }}
</p>
<ui-input
:model-value="value"
:placeholder="!value ? 'EMPTY' : null"
:data-testid="name"
:title="name"
readonly
class="w-full"
>
<template #prepend>
<button
class="absolute ml-2"
@click="copySelector(name, value)"
>
<v-remixicon name="riFileCopyLine" />
</button>
</template>
</ui-input>
</div>
</template>
</selector-element-list>
</ui-tab-panel>
<ui-tab-panel value="options">
<selector-element-list
:elements="selectElements"
element-name="Select element options"
@highlight="
$emit('highlight', {
index: $event.element.elIndex,
highlight: $event.highlight,
})
"
>
<template #item="{ element }">
<div
v-for="option in element.options"
:key="option.name"
class="bg-box-transparent mb-1 rounded-lg py-2 px-3"
>
<p
class="text-overflow text-sm leading-tight text-gray-600"
title="Option name"
>
{{ option.name }}
</p>
<input
:value="option.value"
title="Option value"
class="text-overflow w-full bg-transparent focus:ring-0"
readonly
@click="$event.target.select()"
/>
</div>
</template>
</selector-element-list>
</ui-tab-panel>
<ui-tab-panel value="blocks">
<selector-blocks
:elements="selectedElements"
:selector="elSelector"
@execute="$emit('execute', $event)"
@update="$emit('update')"
/>
</ui-tab-panel>
</ui-tab-panels>
</template>
<script setup>
import { inject } from 'vue';
import SelectorBlocks from './SelectorBlocks.vue';
import SelectorElementList from './SelectorElementList.vue';
const props = defineProps({
activeTab: {
type: String,
default: '',
},
selectElements: {
type: Array,
default: () => [],
},
selectedElements: {
type: Array,
default: () => [],
},
elSelector: {
type: String,
default: '',
},
hideBlocks: Boolean,
});
defineEmits(['update:activeTab', 'execute', 'highlight', 'update']);
const rootElement = inject('rootElement');
function copySelector(name, value) {
rootElement.shadowRoot
.querySelector(`[data-testid="${name}"] input`)
?.select();
const type = rootElement.shadowRoot.querySelector(`select#select--1`)?.value;
navigator.clipboard
.writeText(
type === 'css'
? `${props.selectedElements[0].tagName.toLowerCase()}[${name}="${value}"]`
: `//${props.selectedElements[0].tagName.toLowerCase()}[@${name}='${value}']`
)
.catch((error) => {
document.execCommand('copy');
console.error(error);
});
}
</script>
================================================
FILE: src/components/content/selector/SelectorQuery.vue
================================================
<template>
<div>
<div class="flex items-center">
<ui-select
:model-value="selectorType"
:disabled="selectList"
class="w-full"
@change="$emit('update:selectorType', $event)"
>
<option value="css">CSS Selector</option>
<option value="xpath">XPath</option>
</ui-select>
<template v-if="selectorType === 'css'">
<ui-button
:class="{ 'text-primary': selectList }"
icon
class="ml-2"
title="Select a list of elements"
@click.stop.prevent="$emit('update:selectList', !selectList)"
>
<v-remixicon name="riListUnordered" />
</ui-button>
<ui-button
icon
class="ml-2"
title="Selector settings"
@click="$emit('settings', !settingsActive)"
>
<v-remixicon
:name="settingsActive ? 'riCloseLine' : 'riSettings3Line'"
/>
</ui-button>
</template>
</div>
<div class="mt-2 flex items-center">
<ui-input
:model-value="selector"
placeholder="Element selector"
class="element-selector h-full flex-1 leading-normal"
@change="$emit('selector', $event)"
>
<template #prepend>
<button
class="absolute left-0 ml-2"
@click.stop.prevent="copySelector"
>
<v-remixicon name="riFileCopyLine" />
</button>
</template>
</ui-input>
<template v-if="selectedCount === 1 && !selector.includes('|>')">
<button
class="mr-1 ml-2"
title="Parent element"
@click.stop.prevent="$emit('parent')"
>
<v-remixicon rotate="90" name="riArrowLeftLine" />
</button>
<button title="Child element" @click.stop.prevent="$emit('child')">
<v-remixicon rotate="-90" name="riArrowLeftLine" />
</button>
</template>
</div>
</div>
</template>
<script setup>
import { inject } from 'vue';
import UiInput from '@/components/ui/UiInput.vue';
const props = defineProps({
selector: {
type: String,
default: '',
},
selectedCount: {
type: Number,
default: 0,
},
selectorType: {
type: String,
default: '',
},
selectList: {
type: Boolean,
default: false,
},
settingsActive: Boolean,
});
defineEmits([
'change',
'list',
'parent',
'child',
'selector',
'settings',
'update:selectorType',
'update:selectList',
]);
const rootElement = inject('rootElement');
function copySelector() {
rootElement.shadowRoot.querySelector('input')?.select();
navigator.clipboard.writeText(props.selector).catch((error) => {
document.execCommand('copy');
console.error(error);
});
}
</script>
================================================
FILE: src/components/content/shared/SharedElementHighlighter.vue
================================================
<template>
<rect
v-for="(item, index) in items"
v-bind="{
x: getNumber(item?.x),
y: getNumber(item?.y),
fill: getFillColor(item),
stroke: getStrokeColor(item),
width: getNumber(item?.width),
height: getNumber(item?.height),
'stroke-dasharray': item?.outline ? '5,5' : null,
}"
:key="index"
stroke-width="2"
></rect>
</template>
<script setup>
const props = defineProps({
items: {
type: Object,
default: () => ({}),
},
stroke: {
type: String,
default: null,
},
activeStroke: {
type: String,
default: null,
},
fill: {
type: String,
default: null,
},
activeFill: {
type: String,
default: null,
},
});
function getNumber(num) {
if (Number.isNaN(num) || !num) return 0;
return num;
}
function getFillColor(item) {
if (!item) return null;
if (item.outline) return null;
return item.highlight ? props.fill : props.activeFill || props.fill;
}
function getStrokeColor(item) {
if (!item) return null;
return item.highlight ? props.stroke : props.activeStroke || props.stroke;
}
</script>
================================================
FILE: src/components/content/shared/SharedElementSelector.vue
================================================
<template>
<svg
v-if="!disabled"
class="automa-element-highlighter"
style="
height: 100%;
width: 100%;
top: 0;
left: 0;
pointer-events: none;
position: fixed;
z-index: 999999;
"
>
<shared-element-highlighter
:items="elementsState.hovered"
stroke="#fbbf24"
fill="rgba(251, 191, 36, 0.1)"
/>
<shared-element-highlighter
:items="elementsState.selected"
stroke="#2563EB"
active-stroke="#f87171"
fill="rgba(37, 99, 235, 0.1)"
active-fill="rgba(248, 113, 113, 0.1)"
/>
</svg>
<teleport to="html">
<div
v-if="!disabled"
id="automa-selector-overlay"
style="
z-index: 9999999;
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
"
></div>
</teleport>
</template>
<script setup>
import { reactive, watch, onBeforeUnmount, toRaw } from 'vue';
import { finder } from '@medv/finder';
import { debounce } from '@/utils/helper';
import getSelectorOptions from '@/content/elementSelector/getSelectorOptions';
import { generateXPath, getElementPath, getElementRect } from '@/content/utils';
import findElementList from '@/content/elementSelector/listSelector';
import generateElementsSelector from '@/content/elementSelector/generateElementsSelector';
import SharedElementHighlighter from './SharedElementHighlighter.vue';
const props = defineProps({
selectorType: {
type: String,
default: 'css',
},
selectedEls: {
type: Array,
default: () => [],
},
selectorSettings: {
type: Object,
default: () => ({}),
},
list: Boolean,
hide: Boolean,
pause: Boolean,
disabled: Boolean,
onlyInList: Boolean,
withAttributes: Boolean,
});
const emit = defineEmits(['selected']);
let frameElement = null;
let frameElementRect = null;
let lastScrollPosY = window.scrollY;
let lastScrollPosX = window.scrollX;
const mousePosition = {
x: 0,
y: 0,
};
let hoveredElements = [];
const elementsState = reactive({
hovered: [],
selected: [],
});
const onScroll = debounce(() => {
if (props.disabled) return;
hoveredElements = [];
elementsState.hovered = [];
const yPos = window.scrollY - lastScrollPosY;
const xPos = window.scrollX - lastScrollPosX;
elementsState.selected.forEach((_, index) => {
elementsState.selected[index].x -= xPos;
elementsState.selected[index].y -= yPos;
});
lastScrollPosX = window.scrollX;
lastScrollPosY = window.scrollY;
}, 100);
function getElementRectWithOffset(
element,
{ withAttribute, withElOptions } = {}
) {
const rect = getElementRect(element, withAttribute);
if (frameElementRect) {
rect.y += frameElementRect.top;
rect.x += frameElementRect.left;
}
if (withElOptions && element.tagName === 'SELECT') {
rect.options = Array.from(element.querySelectorAll('option')).map((el) => ({
value: el.value,
name: el.innerText,
}));
}
return rect;
}
function removeElementsList() {
const prevSelectedList = document.querySelectorAll('[automa-el-list]');
prevSelectedList.forEach((el) => {
el.removeAttribute('automa-el-list');
});
}
function resetFramesElements(options = {}) {
const elements = document.querySelectorAll('iframe, frame');
elements.forEach((element) => {
element.contentWindow.postMessage(
{
...options,
type: 'automa:reset-element-selector',
},
'*'
);
});
}
function retrieveElementsRect({ clientX, clientY, target: eventTarget }, type) {
const isAutomaContainer = eventTarget.classList.contains(
'automa-element-selector'
);
if (props.disabled || isAutomaContainer) return;
const isSelectList = props.list && props.selectorType === 'css';
let { 1: target } = document.elementsFromPoint(clientX, clientY);
if (!target) return;
const onlyInList = props.onlyInList && elementsState.selected.length > 0;
const framesEl = ['IFRAME', 'FRAME'];
if (framesEl.includes(target.tagName)) {
if (type === 'selected') removeElementsList();
if (target.contentDocument) {
frameElement = target;
frameElementRect = target.getBoundingClientRect();
const yPos = clientY - frameElementRect.top;
const xPos = clientX - frameElementRect.left;
target = target.contentDocument.elementFromPoint(xPos, yPos);
} else {
const { top, left } = target.getBoundingClientRect();
const payload = {
top,
left,
clientX,
clientY,
onlyInList,
list: isSelectList,
type: 'automa:get-element-rect',
withAttributes: props.withAttributes,
};
if (type === 'selected') {
Object.assign(payload, {
click: true,
selectorType: props.selectorType,
selectorSettings: toRaw(props.selectorSettings),
});
}
target.contentWindow.postMessage(payload, '*');
frameElement = target;
frameElementRect = target.getBoundingClientRect();
return;
}
} else {
frameElement = null;
frameElementRect = null;
}
let elementsRect = [];
const withElOptions = type === 'selected';
const withAttribute = props.withAttributes && type === 'selected';
if (isSelectList) {
const elements =
findElementList(target, {
onlyInList,
frameElement,
}) || [];
if (type === 'hovered') hoveredElements = elements;
elementsRect = elements.map((el) =>
getElementRectWithOffset(el, { withAttribute, withElOptions })
);
} else {
if (type === 'hovered') hoveredElements = [target];
elementsRect = [
getElementRectWithOffset(target, { withAttribute, withElOptions }),
];
}
elementsState[type] = elementsRect;
if (type === 'selected') {
if (!frameElement) resetFramesElements();
const selectorOptions = getSelectorOptions(props.selectorSettings);
let selector = generateElementsSelector({
target,
frameElement,
hoveredElements,
list: isSelectList,
selectorType: props.selectorType,
selectorSettings: selectorOptions,
});
if (frameElement) {
const frameSelector = finder(frameElement, selectorOptions);
selector = `${frameSelector} |> ${selector}`;
}
const selectElements = elementsRect.reduce((acc, rect, index) => {
if (rect.tagName !== 'SELECT') return acc;
acc.push({ ...rect, elIndex: index });
return acc;
}, []);
emit('selected', {
selector,
selectElements,
elements: elementsRect,
path: getElementPath(target),
});
}
}
function onMousemove(event) {
if (props.pause) return;
mousePosition.x = event.clientX;
mousePosition.y = event.clientY;
retrieveElementsRect(event, 'hovered');
}
function onKeydown(event) {
if (props.pause || event.repeat || event.code !== 'Space') return;
const { 1: selectedElement } = document.elementsFromPoint(
mousePosition.x,
mousePosition.y
);
if (selectedElement.id === 'automa-selector-overlay') return;
event.preventDefault();
event.stopPropagation();
retrieveElementsRect(
{
target: selectedElement,
clientX: mousePosition.x,
clientY: mousePosition.y,
},
'selected'
);
}
function onMousedown(event) {
if (event.target.id === 'automa-selector-overlay') {
event.preventDefault();
event.stopPropagation();
}
retrieveElementsRect(event, 'selected');
}
function onMessage({ data }) {
if (data.type !== 'automa:iframe-element-rect') return;
if (data.click) {
const frameSelector =
props.selectorType === 'css'
? finder(frameElement, { tagName: () => true })
: generateXPath(frameElement);
emit('selected', {
elements: data.elements,
selector: `${frameSelector} |> ${data.selector}`,
});
}
const key = data.click ? 'selected' : 'hovered';
elementsState[key] = data.elements;
}
function attachListeners() {
window.addEventListener('scroll', onScroll);
window.addEventListener('message', onMessage);
document.addEventListener('keydown', onKeydown);
window.addEventListener('mousemove', onMousemove);
document.addEventListener('mousedown', onMousedown);
}
function detachListeners() {
window.removeEventListener('scroll', onScroll);
window.removeEventListener('message', onMessage);
document.removeEventListener('keydown', onKeydown);
window.removeEventListener('mousemove', onMousemove);
document.removeEventListener('mousedown', onMousedown);
}
watch(
() => [props.list, props.disabled],
() => {
removeElementsList();
resetFramesElements({ clearCache: true });
}
);
watch(
() => props.selectedEls,
() => {
elementsState.selected = props.selectedEls;
}
);
watch(
() => props.hide,
() => {
if (!props.hide) attachListeners();
else detachListeners();
},
{ immediate: true }
);
onBeforeUnmount(detachListeners);
</script>
================================================
FILE: src/components/newtab/app/AppLogs.vue
================================================
<template>
<ui-modal
v-model="state.show"
custom-content
content-position="start"
@close="clearState"
>
<ui-card class="mt-8 w-full" style="max-width: 1400px; min-height: 600px">
<app-logs-items
v-if="!state.logId"
:workflow-id="state.workflowId"
@select="onSelectLog"
@close="clearState"
/>
<app-logs-item-running
v-else-if="state.runningWorkflow"
:log-id="state.logId"
@close="closeItemPage"
/>
<app-logs-item v-else :log-id="state.logId" @close="closeItemPage" />
</ui-card>
</ui-modal>
</template>
<script setup>
import { reactive } from 'vue';
import emitter from '@/lib/mitt';
import AppLogsItem from './AppLogsItem.vue';
import AppLogsItems from './AppLogsItems.vue';
import AppLogsItemRunning from './AppLogsItemRunning.vue';
const state = reactive({
logId: '',
source: '',
show: false,
workflowId: '',
runningWorkflow: false,
});
emitter.on('ui:logs', (event = {}) => {
Object.assign(state, event);
});
function clearState() {
state.show = false;
state.logId = '';
state.source = '';
state.runningWorkflow = false;
}
function closeItemPage(closeModal = false) {
state.logId = '';
if (closeModal) clearState();
}
function onSelectLog({ id, type }) {
state.runningWorkflow = type === 'running';
state.logId = id;
}
</script>
================================================
FILE: src/components/newtab/app/AppLogsItem.vue
================================================
<template>
<div v-if="currentLog.id">
<div class="flex items-center">
<button
v-tooltip:bottom="t('workflow.blocks.go-back.name')"
role="button"
class="bg-input mr-2 h-12 rounded-lg px-1 text-gray-600 transition dark:text-gray-300"
@click="$emit('close')"
>
<v-remixicon name="riArrowLeftSLine" />
</button>
<div>
<h1 class="text-overflow max-w-md text-2xl font-semibold">
{{ currentLog.name }}
</h1>
<p class="text-gray-600 dark:text-gray-200">
{{
t(`log.description.text`, {
status: t(
`log.description.status.${currentLog.status || 'success'}`
),
date: dayjs(currentLog.startedAt).format('DD MMM'),
duration: countDuration(currentLog.startedAt, currentLog.endedAt),
})
}}
</p>
</div>
<div class="grow"></div>
<ui-button
v-if="state.workflowExists"
v-tooltip="t('log.goWorkflow')"
icon
class="mr-4"
@click="goToWorkflow"
>
<v-remixicon name="riExternalLinkLine" />
</ui-button>
<ui-button class="text-red-500 dark:text-red-400" @click="deleteLog">
{{ t('common.delete') }}
</ui-button>
</div>
<ui-tabs v-model="state.activeTab" class="mt-4" @change="onTabChange">
<ui-tab v-for="tab in tabs" :key="tab.id" class="mr-4" :value="tab.id">
{{ tab.name }}
</ui-tab>
</ui-tabs>
<ui-tab-panels
:model-value="state.activeTab"
class="scroll mt-4 overflow-auto px-2 pb-4"
style="min-height: 500px; max-height: calc(100vh - 15rem)"
>
<ui-tab-panel value="logs">
<logs-history
:current-log="currentLog"
:ctx-data="ctxData"
:parent-log="parentLog"
/>
</ui-tab-panel>
<ui-tab-panel value="table">
<logs-table :current-log="currentLog" :table-data="tableData" />
</ui-tab-panel>
<ui-tab-panel value="variables">
<logs-variables :current-log="currentLog" />
</ui-tab-panel>
</ui-tab-panels>
</div>
</template>
<script setup>
import { shallowReactive, shallowRef, watch } from 'vue';
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import dbLogs from '@/db/logs';
import dayjs from '@/lib/dayjs';
import { useWorkflowStore } from '@/stores/workflow';
import { countDuration, convertArrObjTo2DArr } from '@/utils/helper';
import LogsTable from '@/components/newtab/logs/LogsTable.vue';
import LogsHistory from '@/components/newtab/logs/LogsHistory.vue';
import LogsVariables from '@/components/newtab/logs/LogsVariables.vue';
const props = defineProps({
logId: {
type: String,
default: '',
},
});
const emit = defineEmits(['close']);
const { t } = useI18n();
const router = useRouter();
const workflowStore = useWorkflowStore();
const ctxData = shallowRef({});
const parentLog = shallowRef(null);
const tabs = [
{ id: 'logs', name: t('common.log', 2) },
{ id: 'table', name: t('workflow.table.title') },
{ id: 'variables', name: t('workflow.variables.title', 2) },
];
const state = shallowReactive({
activeTab: 'logs',
workflowExists: false,
});
const tableData = shallowReactive({
converted: false,
body: [],
header: [],
});
const currentLog = shallowRef({
history: [],
data: {
table: [],
variables: {},
},
});
function deleteLog() {
dbLogs.items
.where('id')
.equals(props.logId)
.delete()
.then(() => {
emit('close');
});
}
function goToWorkflow() {
const path = `/workflows/${currentLog.value.workflowId}`;
router.push(path);
emit('close', true);
}
function convertToTableData() {
const data = currentLog.value.data?.table;
if (!data) return;
const [header] = convertArrObjTo2DArr(data);
tableData.converted = true;
tableData.body = data.map((item, index) => ({ ...item, id: index + 1 }));
tableData.header = header.map((name) => ({
text: name,
value: name,
filterable: true,
}));
tableData.header.unshift({ value: 'id', text: '', sortable: false });
}
function onTabChange(value) {
if (value === 'table' && !tableData.converted) {
convertToTableData();
}
}
async function fetchLog() {
if (!props.logId) return;
const logDetail = await dbLogs.items.where('id').equals(props.logId).last();
if (!logDetail) return;
tableData.body = [];
tableData.header = [];
parentLog.value = null;
tableData.converted = false;
const [logCtxData, logHistory, logsData] = await Promise.all(
['ctxData', 'histories', 'logsData'].map((key) =>
dbLogs[key].where('logId').equals(props.logId).last()
)
);
ctxData.value = logCtxData?.data || {};
currentLog.value = {
history: logHistory?.data || [],
data: logsData?.data || {},
...logDetail,
};
state.workflowExists = Boolean(workflowStore.getById(logDetail.workflowId));
const parentLogId = logDetail.collectionLogId || logDetail.parentLog?.id;
if (parentLogId) {
parentLog.value =
(await dbLogs.items.where('id').equals(parentLogId).last()) || null;
}
}
watch(() => props.logId, fetchLog, { immediate: true });
</script>
<style>
.logs-details .cm-editor {
max-height: calc(100vh - 15rem);
}
</style>
================================================
FILE: src/components/newtab/app/AppLogsItemRunning.vue
================================================
<template>
<div v-if="running">
<div class="flex items-center">
<button
v-tooltip:bottom="t('workflow.blocks.go-back.name')"
role="button"
class="bg-input mr-2 h-12 rounded-lg px-1 text-gray-600 transition dark:text-gray-300"
@click="$emit('close')"
>
<v-remixicon name="riArrowLeftSLine" />
</button>
<div class="grow overflow-hidden">
<h1 class="text-overflow max-w-md text-2xl font-semibold">
{{ running.state.name }}
</h1>
<p>
{{
t('running.start', {
date: dayjs(running.state.startedTimestamp).format(
'DD MMM, hh:mm A'
),
})
}}
</p>
</div>
<ui-button @click="stopWorkflow">
{{ t('common.stop') }}
</ui-button>
</div>
<div class="mt-8">
<logs-history
:is-running="true"
:current-log="{
history: running.state.logs,
workflowId: running.workflowId,
}"
>
<template #header-prepend>
<div>
<h3 class="leading-tight">
{{ t('common.log', 2) }}
</h3>
<p class="leading-tight text-gray-600 dark:text-gray-300">
{{ t('running.message') }}
</p>
</div>
</template>
<template #append-items>
<div
v-for="block in running.state.currentBlock"
:key="block.id"
class="hoverable group flex w-full items-center rounded-md px-2 py-1"
>
<span
:key="key"
:title="`Duration: ${Math.round(
(Date.now() - block.startedAt) / 1000
)}s`"
class="text-overflow ml-6 w-14 shrink-0 text-gray-400"
>
{{ countDuration(block.startedAt, Date.now()) }}
</span>
<ui-spinner size="16" class="mr-2" color="text-accent" />
<p class="flex-1">
{{ t(`workflow.blocks.${block.name}.name`) }}
</p>
</div>
</template>
</logs-history>
</div>
</div>
</template>
<script setup>
import { computed, watch, shallowRef, onBeforeUnmount } from 'vue';
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { countDuration } from '@/utils/helper';
import { useWorkflowStore } from '@/stores/workflow';
import dbLogs from '@/db/logs';
import dayjs from '@/lib/dayjs';
import LogsHistory from '@/components/newtab/logs/LogsHistory.vue';
import RendererWorkflowService from '@/service/renderer/RendererWorkflowService';
const props = defineProps({
logId: {
type: String,
default: '',
},
});
const emit = defineEmits(['close']);
const { t } = useI18n();
const router = useRouter();
const workflowStore = useWorkflowStore();
const key = shallowRef(0);
const interval = setInterval(() => {
key.value += 1;
}, 1000);
const running = computed(() =>
workflowStore.getAllStates.find(({ id }) => id === props.logId)
);
function stopWorkflow() {
RendererWorkflowService.stopWorkflowExecution(running.value.id);
emit('close');
}
watch(
running,
async () => {
if (!running.value && props.logId) {
const log = await dbLogs.items.where('id').equals(props.logId).first();
let path = '/logs';
if (log) {
path = `/logs/${props.logId}`;
}
router.replace(path);
}
},
{ immediate: true }
);
onBeforeUnmount(() => {
clearInterval(interval);
});
</script>
================================================
FILE: src/components/newtab/app/AppLogsItems.vue
================================================
<template>
<div class="logs-list overflow-auto pb-4 pt-1">
<div class="mb-8 flex items-center">
<h1 class="flex-1 text-2xl font-semibold">
{{ $t('common.log', 2) }}
</h1>
<v-remixicon
name="riCloseLine"
class="cursor-pointer text-gray-600 dark:text-gray-300"
@click="$emit('close')"
/>
</div>
<logs-filters
:sorts="sortsBuilder"
:filters="filtersBuilder"
@clear="clearLogs"
@updateSorts="sortsBuilder[$event.key] = $event.value"
@updateFilters="filtersBuilder[$event.key] = $event.value"
>
<ui-popover padding="" @click="filtersBuilder.workflowQuery = ''">
<template #trigger>
<ui-button>
<span class="text-overflow text-left" style="max-width: 160px">
{{ activeWorkflowName }}
</span>
<v-remixicon name="riArrowDropDownLine" class="-mr-1 ml-2" />
</ui-button>
</template>
<div class="w-64">
<div class="p-4">
<ui-input
v-model="filtersBuilder.workflowQuery"
autofocus
placeholder="Search..."
class="w-full"
prepend-icon="riSearch2Line"
/>
<div class="text-right">
<span
class="cursor-pointer text-sm text-gray-600 underline dark:text-gray-300"
@click="filtersBuilder.workflowId = ''"
>
Clear
</span>
</div>
</div>
<ui-list class="scroll mb-4 max-h-96 space-y-1 overflow-auto px-4">
<ui-list-item
v-for="workflow in workflows"
:key="workflow.id"
:active="filtersBuilder.workflowId === workflow.id"
class="cursor-pointer"
@click="filtersBuilder.workflowId = workflow.id"
>
<p class="text-overflow">{{ workflow.name }}</p>
</ui-list-item>
</ui-list>
</div>
</ui-popover>
</logs-filters>
<div v-if="logs" style="min-height: 320px">
<shared-logs-table
:logs="logs"
:modal="true"
:running="workflowStates"
class="w-full"
style="max-height: calc(100vh - 18rem)"
@select="$emit('select', $event)"
>
<template #item-prepend="{ log }">
<td class="w-8">
<ui-checkbox
:model-value="selectedLogs.includes(log.id)"
class="align-text-bottom"
@change="toggleSelectedLog($event, log.id)"
/>
</td>
</template>
<template #item-append="{ log }">
<td class="ml-4 text-right">
<v-remixicon
name="riDeleteBin7Line"
class="inline-block cursor-pointer text-red-500 dark:text-red-400"
@click="deleteLog(log.id)"
/>
</td>
</template>
</shared-logs-table>
</div>
<div class="mt-4 md:flex md:items-center md:justify-between">
<div>
{{ t('components.pagination.text1') }}
<select v-model="pagination.perPage" class="bg-input rounded-md p-1">
<option v-for="num in [10, 15, 25, 50, 100]" :key="num" :value="num">
{{ num }}
</option>
</select>
{{ t('components.pagination.text2', { count: filteredLogs.length }) }}
</div>
<ui-pagination
v-model="pagination.currentPage"
:per-page="pagination.perPage"
:records="filteredLogs.length"
class="mt-4 md:mt-0"
/>
</div>
<ui-card
v-if="selectedLogs.length !== 0"
class="fixed right-0 bottom-0 m-5 space-x-2 shadow-xl"
>
<ui-button @click="selectAllLogs">
{{
t(
`log.${
selectedLogs.length >= logs?.length ? 'deselectAll' : 'selectAll'
}`
)
}}
</ui-button>
<ui-button variant="danger" @click="deleteSelectedLogs">
{{ t('log.deleteSelected') }} ({{ selectedLogs.length }})
</ui-button>
</ui-card>
<ui-modal v-model="exportDataModal.show" content-class="max-w-2xl">
<template #header>
<span class="capitalize">{{ t('common.data') }}</span>
</template>
<logs-data-viewer
:log="exportDataModal.log"
editor-class="logs-list-data"
/>
</ui-modal>
</div>
</template>
<script setup>
import { shallowReactive, ref, computed, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useDialog } from '@/composable/dialog';
import dbLogs from '@/db/logs';
import { useWorkflowStore } from '@/stores/workflow';
import { useHostedWorkflowStore } from '@/stores/hostedWorkflow';
import { useLiveQuery } from '@/composable/liveQuery';
import LogsFilters from '@/components/newtab/logs/LogsFilters.vue';
import LogsDataViewer from '@/components/newtab/logs/LogsDataViewer.vue';
import SharedLogsTable from '@/components/newtab/shared/SharedLogsTable.vue';
const props = defineProps({
workflowId: {
type: String,
default: '',
},
});
defineEmits(['select', 'close']);
const { t } = useI18n();
const dialog = useDialog();
const workflowStore = useWorkflowStore();
const hostedWorkflows = useHostedWorkflowStore();
const storedlogs = useLiveQuery(() => dbLogs.items.toArray());
const savedSorts = JSON.parse(localStorage.getItem('logs-sorts') || '{}');
const selectedLogs = ref([]);
const pagination = shallowReactive({
perPage: 10,
currentPage: 1,
});
const filtersBuilder = shallowReactive({
query: '',
byDate: 0,
byStatus: 'all',
workflowQuery: '',
workflowId: props.workflowId,
});
const sortsBuilder = shallowReactive({
order: savedSorts.order || 'desc',
by: savedSorts.by || 'endedAt',
});
const exportDataModal = shallowReactive({
show: false,
log: {},
});
const allWorkflows = computed(() =>
[...hostedWorkflows.toArray, ...workflowStore.getWorkflows].sort((a, b) =>
a.createdAt > b.createdAt ? -1 : 1
)
);
const workflows = computed(() =>
allWorkflows.value.filter((workflow) =>
workflow.name
.toLocaleLowerCase()
.includes(filtersBuilder.workflowQuery.toLocaleLowerCase())
)
);
const activeWorkflowName = computed(() => {
if (!filtersBuilder.workflowId) return 'All workflows';
const workflow = allWorkflows.value.find(
(item) => item.id === filtersBuilder.workflowId
);
return workflow?.name ?? 'All workflows';
});
const workflowStates = computed(() => {
const states = workflowStore.getAllStates;
if (!filtersBuilder.workflowId) return states;
return states.filter(
(state) => state.workflowId === filtersBuilder.workflowId
);
});
const filteredLogs = computed(() => {
if (!storedlogs.value) return [];
return storedlogs.value
.filter(({ name, status, endedAt, workflowId }) => {
let dateFilter = true;
let statusFilter = true;
const workflowIdFilter = filtersBuilder.workflowId
? filtersBuilder.workflowId === workflowId
: true;
const searchFilter = name
.toLocaleLowerCase()
.includes(filtersBuilder.query.toLocaleLowerCase());
if (filtersBuilder.byStatus !== 'all') {
statusFilter = status === filtersBuilder.byStatus;
}
if (filtersBuilder.byDate > 0) {
const date = Date.now() - filtersBuilder.byDate * 24 * 60 * 60 * 1000;
dateFilter = date <= endedAt;
}
return searchFilter && workflowIdFilter && statusFilter && dateFilter;
})
.slice()
.sort((a, b) => {
const valueA = a[sortsBuilder.by];
const valueB = b[sortsBuilder.by];
if (sortsBuilder.order === 'asc') return valueA > valueB ? 1 : -1;
return valueB > valueA ? 1 : -1;
});
});
const logs = computed(() =>
filteredLogs.value.slice(
(pagination.currentPage - 1) * pagination.perPage,
pagination.currentPage * pagination.perPage
)
);
function deleteLog(id) {
dbLogs.items.delete(id).then(() => {
dbLogs.ctxData.where('logId').equals(id).delete();
dbLogs.histories.where('logId').equals(id).delete();
dbLogs.logsData.where('logId').equals(id).delete();
});
}
function toggleSelectedLog(selected, logId) {
if (selected) {
selectedLogs.value.push(logId);
return;
}
const index = selectedLogs.value.indexOf(logId);
if (index !== -1) selectedLogs.value.splice(index, 1);
}
function deleteSelectedLogs() {
dialog.confirm({
title: t('log.delete.title'),
okVariant: 'danger',
body: t('log.delete.description'),
onConfirm: () => {
dbLogs.items.bulkDelete(selectedLogs.value).then(() => {
selectedLogs.value = [];
});
},
});
}
function clearLogs() {
dialog.confirm({
title: t('log.clearLogs.title'),
okVariant: 'danger',
body: t('log.clearLogs.description'),
onConfirm: () => {
dbLogs.items.clear();
dbLogs.ctxData.clear();
dbLogs.logsData.clear();
dbLogs.histories.clear();
},
});
}
function selectAllLogs() {
if (selectedLogs.value.length >= logs.value?.length) {
selectedLogs.value = [];
return;
}
const logIds = logs?.value.map(({ id }) => id);
selectedLogs.value = logIds;
}
watch(
() => sortsBuilder,
(value) => {
localStorage.setItem('logs-sorts', JSON.stringify(value));
},
{ deep: true }
);
</script>
<style>
.logs-list-data {
max-height: calc(100vh - 12rem);
}
</style>
================================================
FILE: src/components/newtab/app/AppSidebar.vue
================================================
<template>
<aside
class="fixed left-0 top-0 z-50 flex h-screen w-16 flex-col items-center bg-white py-6 dark:bg-gray-800"
>
<img
:title="`v${extensionVersion}`"
src="@/assets/svg/logo.svg"
class="mx-auto mb-4 w-10"
/>
<div
class="relative w-full space-y-2 text-center"
@mouseleave="showHoverIndicator = false"
>
<div
v-show="showHoverIndicator"
ref="hoverIndicator"
class="bg-box-transparent absolute left-1/2 h-10 w-10 rounded-lg transition-transform duration-200"
style="transform: translate(-50%, 0)"
></div>
<router-link
v-for="tab in tabs"
v-slot="{ href, navigate, isActive }"
:key="tab.id"
:to="tab.path"
custom
>
<a
v-tooltip:right.group="
`${t(`common.${tab.id}`, 2)} ${
tab.shortcut && `(${tab.shortcut.readable})`
}`
"
:class="{ 'is-active': isActive }"
:href="tab.id === 'log' ? '#' : href"
class="tab relative z-10 flex w-full items-center justify-center"
@click="navigateLink($event, navigate, tab)"
@mouseenter="hoverHandler"
>
<div class="inline-block rounded-lg p-2 transition-colors">
<v-remixicon :name="tab.icon" />
</div>
<span
v-if="tab.id === 'log' && runningWorkflowsLen > 0"
class="absolute -top-1 right-2 h-4 w-4 rounded-full bg-accent text-xs text-white dark:text-black"
>
{{ runningWorkflowsLen }}
</span>
</a>
</router-link>
</div>
<hr class="my-4 w-8/12" />
<button
v-tooltip:right.group="$t('home.elementSelector.name')"
class="focus:ring-0"
@click="injectElementSelector"
>
<v-remixicon name="riFocus3Line" />
</button>
<div class="grow"></div>
<router-link
v-if="userStore.user"
v-tooltip:right.group="t('settings.menu.profile')"
to="/profile"
class="bg-box-transparent inline-block rounded-full p-1 transition-transform hover:scale-110"
>
<img
:src="userStore.user.avatar_url"
height="32"
width="32"
class="rounded-full"
alt="User avatar"
/>
</router-link>
<ui-popover trigger="mouseenter" placement="right" class="my-4">
<template #trigger>
<v-remixicon name="riGroupLine" />
</template>
<p class="mb-2">{{ t('home.communities') }}</p>
<ui-list class="w-40">
<ui-list-item
v-for="item in communities"
:key="item.name"
:href="item.url"
small
tag="a"
target="_blank"
rel="noopener"
>
<v-remixicon :name="item.icon" class="mr-2" />
{{ item.name }}
</ui-list-item>
</ui-list>
</ui-popover>
<router-link v-tooltip:right.group="t('settings.menu.about')" to="/about">
<v-remixicon class="cursor-pointer" name="riInformationLine" />
</router-link>
</aside>
</template>
<script setup>
import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { useToast } from 'vue-toastification';
import browser from 'webextension-polyfill';
import { useUserStore } from '@/stores/user';
import { useWorkflowStore } from '@/stores/workflow';
import { useShortcut, getShortcut } from '@/composable/shortcut';
import { useGroupTooltip } from '@/composable/groupTooltip';
import { communities } from '@/utils/shared';
import { initElementSelector } from '@/newtab/utils/elementSelector';
import emitter from '@/lib/mitt';
useGroupTooltip();
const { t } = useI18n();
const toast = useToast();
const router = useRouter();
const userStore = useUserStore();
const workflowStore = useWorkflowStore();
const extensionVersion = browser.runtime.getManifest().version;
const tabs = [
{
id: 'workflow',
icon: 'riFlowChart',
path: '/workflows',
shortcut: getShortcut('page:workflows', '/workflows'),
},
{
id: 'packages',
icon: 'mdiPackageVariantClosed',
path: '/packages',
shortcut: '',
},
{
id: 'schedule',
icon: 'riTimeLine',
path: '/schedule',
shortcut: getShortcut('page:schedule', '/triggers'),
},
{
id: 'storage',
icon: 'riHardDrive2Line',
path: '/storage',
shortcut: getShortcut('page:storage', '/storage'),
},
{
id: 'log',
icon: 'riHistoryLine',
path: '/logs',
shortcut: getShortcut('page:logs', '/logs'),
},
{
id: 'settings',
icon: 'riSettings3Line',
path: '/settings',
shortcut: getShortcut('page:settings', '/settings'),
},
];
const hoverIndicator = ref(null);
const showHoverIndicator = ref(false);
const runningWorkflowsLen = computed(() => workflowStore.getAllStates.length);
useShortcut(
tabs.reduce((acc, { shortcut }) => {
if (shortcut) {
acc.push(shortcut);
}
return acc;
}, []),
({ data }) => {
if (!data) return;
if (data.includes('/logs')) {
emitter.emit('ui:logs', { show: true });
return;
}
router.push(data);
}
);
function navigateLink(event, navigateFn, tab) {
event.preventDefault();
if (tab.id === 'log') {
emitter.emit('ui:logs', { show: true });
} else {
navigateFn();
}
}
function hoverHandler({ target }) {
showHoverIndicator.value = true;
hoverIndicator.value.style.transform = `translate(-50%, ${target.offsetTop}px)`;
}
async function injectElementSelector() {
try {
const [tab] = await browser.tabs.query({ active: true, url: '*://*/*' });
if (!tab) {
toast.error(t('home.elementSelector.noAccess'));
return;
}
await initElementSelector();
} catch (error) {
console.error(error);
}
}
</script>
<style scoped>
.tab.is-active:after {
content: '';
position: absolute;
right: 0;
top: 0;
height: 100%;
width: 4px;
@apply bg-accent dark:bg-gray-100;
}
</style>
================================================
FILE: src/components/newtab/app/AppSurvey.vue
================================================
<template>
<ui-card
v-if="modalState.show"
class="group fixed bottom-8 right-8 w-72 border-2 shadow-2xl"
>
<button
class="absolute -right-2 -top-2 scale-0 rounded-full bg-white shadow-md transition group-hover:scale-100"
@click="closeModal"
>
<v-remixicon class="text-gray-600" name="riCloseLine" />
</button>
<h2 class="text-lg font-semibold">
{{ activeModal.title }}
</h2>
<p class="mt-1 text-gray-700 dark:text-gray-100">
{{ activeModal.body }}
</p>
<div class="mt-4 space-y-2">
<ui-button
:href="activeModal.url"
tag="a"
target="_blank"
rel="noopener"
class="block w-full"
variant="accent"
>
{{ activeModal.button }}
</ui-button>
</div>
</ui-card>
</template>
<script setup>
import dayjs from '@/lib/dayjs';
import { computed, onMounted, shallowReactive } from 'vue';
import browser from 'webextension-polyfill';
const modalTypes = {
testimonial: {
title: 'Hi There 👋',
body: 'Thank you for using Automa, and if you have a great experience. Would you like to give us a testimonial?',
button: 'Give Testimonial',
url: 'https://testimonial.to/automa',
},
survey: {
title: "How do you think we're doing?",
body: 'To help us make Automa as best it can be, we need a few minutes of your time to get your feedback.',
button: 'Take Survey',
url: 'https://extension.automa.site/survey',
},
};
const modalState = shallowReactive({
show: true,
type: 'survey',
});
function closeModal() {
let value = true;
if (modalState.type === 'survey') {
value = new Date().toString();
}
modalState.show = false;
localStorage.setItem(`has-${modalState.type}`, value);
}
async function checkModal() {
try {
const { isFirstTime } = await browser.storage.local.get('isFirstTime');
if (isFirstTime) {
modalState.show = false;
localStorage.setItem('has-testimonial', true);
localStorage.setItem('has-survey', Date.now());
return;
}
const survey = localStorage.getItem('has-survey');
if (!survey) return;
const daysDiff = dayjs().diff(survey, 'day');
const showTestimonial =
daysDiff >= 2 && !localStorage.getItem('has-testimonial');
if (showTestimonial) {
modalState.show = true;
modalState.type = 'testimonial';
} else {
modalState.show = false;
}
} catch (error) {
console.error(error);
}
}
const activeModal = computed(() => modalTypes[modalState.type]);
onMounted(checkModal);
</script>
================================================
FILE: src/components/newtab/logs/LogsDataViewer.vue
================================================
<template>
<div v-if="state.status === 'loading'" class="py-8 text-center">
<ui-spinner color="text-primary" />
</div>
<template v-else-if="state.status === 'idle'">
<div class="mb-2 flex items-center">
<ui-input
v-model="state.fileName"
:placeholder="t('common.fileName')"
:title="t('common.fileName')"
/>
<div class="grow"></div>
<ui-popover trigger-width>
<template #trigger>
<ui-button variant="accent">
<span>{{ t('log.exportData.title') }}</span>
<v-remixicon name="riArrowDropDownLine" class="ml-2 -mr-1" />
</ui-button>
</template>
<ui-list class="space-y-1">
<ui-list-item
v-for="type in dataExportTypes"
:key="type.id"
v-close-popover
class="cursor-pointer"
@click="exportData(type.id)"
>
{{ t(`log.exportData.types.${type.id}`) }}
</ui-list-item>
</ui-list>
</ui-popover>
</div>
<ui-tabs v-if="objectHasKey(logsData, 'table')" v-model="state.activeTab">
<ui-tab value="table">
{{ t('workflow.table.title') }}
</ui-tab>
<ui-tab value="variables">
{{ t('workflow.variables.title', 2) }}
</ui-tab>
</ui-tabs>
<shared-codemirror
:model-value="dataStr"
:class="editorClass"
class="rounded-t-none"
lang="json"
readonly
/>
</template>
</template>
<script setup>
import {
shallowReactive,
computed,
defineAsyncComponent,
onMounted,
} from 'vue';
import { useI18n } from 'vue-i18n';
import dbLogs from '@/db/logs';
import { dataExportTypes } from '@/utils/shared';
import { objectHasKey } from '@/utils/helper';
import dataExporter from '@/utils/dataExporter';
const SharedCodemirror = defineAsyncComponent(() =>
import('@/components/newtab/shared/SharedCodemirror.vue')
);
const props = defineProps({
log: {
type: Object,
default: () => ({}),
},
editorClass: {
type: String,
default: '',
},
});
const { t } = useI18n();
const state = shallowReactive({
status: 'loading',
activeTab: 'table',
fileName: props.log.name,
});
const logsData = {
table: '',
variables: '',
};
const dataStr = computed(() => {
if (state.status !== 'idle') return '';
return logsData[state.activeTab] ? logsData[state.activeTab] : '';
});
function exportData(type) {
dataExporter(
logsData?.table || logsData,
{ name: state.fileName, type },
true
);
}
onMounted(async () => {
const data = await dbLogs.logsData.where('logId').equals(props.log.id).last();
if (!data) {
state.status = 'error';
return;
}
Object.keys(data.data).forEach((key) => {
logsData[key] = JSON.stringify(data.data[key], null, 2);
});
state.status = 'idle';
});
</script>
================================================
FILE: src/components/newtab/logs/LogsFilters.vue
================================================
<template>
<div class="mb-6 flex flex-wrap items-center md:space-x-4">
<ui-input
id="search-input"
:model-value="filters.query"
:placeholder="`${t('common.search')}...`"
prepend-icon="riSearch2Line"
class="w-6/12 md:w-auto md:flex-1"
@change="updateFilters('query', $event)"
/>
<slot />
<div class="workflow-sort ml-4 flex w-5/12 items-center md:ml-0 md:w-auto">
<ui-button
icon
class="rounded-r-none border-r border-gray-300"
@click="updateSorts('order', sorts.order === 'asc' ? 'desc' : 'asc')"
>
<v-remixicon
:name="sorts.order === 'asc' ? 'riSortAsc' : 'riSortDesc'"
/>
</ui-button>
<ui-select
:model-value="sorts.by"
:placeholder="t('sort.sortBy')"
@change="updateSorts('by', $event)"
>
<option v-for="sort in sortsList" :key="sort.id" :value="sort.id">
{{ sort.name }}
</option>
</ui-select>
</div>
<ui-popover class="mt-4 md:mt-0">
<template #trigger>
<ui-button>
<v-remixicon name="riFilter2Line" class="mr-2 -ml-1" />
<span>{{ t('log.filter.title') }}</span>
</ui-button>
</template>
<div class="w-48">
<p class="mb-2 flex-1 font-semibold">{{ t('log.filter.title') }}</p>
<p class="mb-2 text-sm text-gray-600 dark:text-gray-200">
{{ t('log.filter.byStatus') }}
</p>
<div class="grid grid-cols-2 gap-2">
<ui-radio
v-for="status in filterByStatus"
:key="status.id"
:model-value="filters.byStatus"
:value="status.id"
class="text-sm capitalize"
@change="updateFilters('byStatus', $event)"
>
{{ status.name }}
</ui-radio>
</div>
<p class="mb-1 mt-3 text-sm text-gray-600 dark:text-gray-200">
{{ t('log.filter.byDate.title') }}
</p>
<ui-select
:model-value="filters.byDate"
class="w-full"
@change="updateFilters('byDate', $event)"
>
<option v-for="date in filterByDate" :key="date.id" :value="date.id">
{{ date.name }}
</option>
</ui-select>
</div>
</ui-popover>
<ui-button class="ml-4 mt-4 md:ml-0 md:mt-0" @click="$emit('clear')">
<v-remixicon name="riDeleteBin7Line" class="mr-2 -ml-1" />
<span>
{{ t('log.clearLogs.title') }}
</span>
</ui-button>
</div>
</template>
<script setup>
import { useI18n } from 'vue-i18n';
defineProps({
filters: {
type: Object,
default: () => ({}),
},
sorts: {
type: Object,
default: () => ({}),
},
workflows: {
type: Array,
default: () => [],
},
});
const emit = defineEmits(['updateSorts', 'updateFilters', 'clear']);
const { t } = useI18n();
const filterByStatus = [
{ id: 'all', name: t('common.all') },
{ id: 'success', name: t('logStatus.success') },
{ id: 'stopped', name: t('logStatus.stopped') },
{ id: 'error', name: t('logStatus.error') },
];
const filterByDate = [
{ id: 0, name: t('common.all') },
{ id: 1, name: t('log.filter.byDate.items.lastDay') },
{ id: 7, name: t('log.filter.byDate.items.last7Days') },
{ id: 30, name: t('log.filter.byDate.items.last30Days') },
];
const sortsList = [
{ id: 'name', name: t('sort.name') },
{ id: 'startedAt', name: t('sort.createdAt') },
];
function updateFilters(key, value) {
emit('updateFilters', { key, value });
}
function updateSorts(key, value) {
emit('updateSorts', { key, value });
}
</script>
================================================
FILE: src/components/newtab/logs/LogsHistory.vue
================================================
<template>
<router-link
v-if="parentLog"
replace
:to="'/logs/' + currentLog.parentLog?.id || currentLog.collectionLogId"
class="mb-4 flex"
>
<v-remixicon name="riArrowLeftLine" class="mr-2" />
{{ t('log.goBack', { name: parentLog.name }) }}
</router-link>
<div class="flex flex-col-reverse items-start lg:flex-row">
<div class="w-full lg:w-auto lg:flex-1">
<div class="dark rounded-lg bg-gray-900 text-gray-100">
<div class="mb-4 flex items-center border-b p-4 text-gray-200">
<div v-if="currentLog.status === 'error' && errorBlock">
<p class="line-clamp leading-tight">
{{ errorBlock.message }}
<a
v-if="errorBlock.messageId"
:href="`https://docs.extension.automa.site/reference/workflow-common-errors.html#${errorBlock.messageId}`"
target="_blank"
title="About the error"
@click.stop
>
<v-remixicon
name="riArrowLeftLine"
size="20"
class="inline-block text-gray-300"
rotate="135"
/>
</a>
</p>
<p class="cursor-pointer" title="Jump to item" @click="jumpToError">
On the {{ errorBlock.name }} block
<v-remixicon
name="riArrowLeftLine"
class="-ml-1 inline-block"
size="18"
rotate="135"
/>
</p>
</div>
<slot name="header-prepend" />
<div class="grow" />
<ui-popover v-if="!isRunning" trigger-width class="mr-4">
<template #trigger>
<ui-button>
<span>
Export <span class="hidden lg:inline-block">logs</span>
</span>
<v-remixicon name="riArrowDropDownLine" class="ml-2 -mr-1" />
</ui-button>
</template>
<ui-list class="space-y-1">
<ui-list-item
v-for="type in dataExportTypes"
:key="type.id"
v-close-popover
class="cursor-pointer"
@click="exportLogs(type.id)"
>
{{ t(`log.exportData.types.${type.id}`) }}
</ui-list-item>
</ui-list>
</ui-popover>
<ui-input
v-if="!isRunning"
v-model="state.search"
:placeholder="t('common.search')"
prepend-icon="riSearch2Line"
/>
</div>
<div
id="log-history"
style="max-height: 500px"
class="scroll overflow-auto p-4"
>
<slot name="prepend" />
<p
v-if="currentLog.history.length === 0"
class="text-center text-gray-300"
>
The workflow log is not saved
</p>
<div class="w-full space-y-1 overflow-auto font-mono text-sm">
<div
v-for="(item, index) in history"
:key="item.id || index"
:disabled="!ctxData[item.id]"
:class="{ 'bg-box-transparent': item.id === state.itemId }"
hide-header-icon
class="hoverable group flex w-full cursor-default items-start rounded-md px-2 py-1 text-left focus:ring-0"
@click="setActiveLog(item)"
>
<div
style="min-width: 54px"
class="text-overflow mr-4 shrink-0 text-gray-400"
>
<span
v-if="item.timestamp"
:title="
dayjs(item.timestamp).format('YYYY-MM-DDTHH:mm:ss.SSS')
"
>
{{ dayjs(item.timestamp).format('HH:mm:ss') }}
{{ `(${countDuration(0, item.duration || 0).trim()})` }}
</span>
<span v-else :title="`${Math.round(item.duration / 1000)}s`">
{{ countDuration(0, item.duration || 0) }}
</span>
</div>
<span
:class="logsType[item.type]?.color"
:title="item.type"
class="text-overflow w-2/12 shrink-0"
>
<v-remixicon
:name="logsType[item.type]?.icon"
size="18"
class="-mr-1 inline-block align-text-top"
/>
{{ item.name }}
</span>
<span
:title="`${t('common.description')} (${item.description})`"
class="text-overflow ml-2 w-2/12 shrink-0"
>
{{ item.description }}
</span>
<p
:title="item.message"
class="line-clamp ml-2 flex-1 text-sm leading-tight text-gray-600 dark:text-gray-200"
>
{{ item.message }}
<a
v-if="item.messageId"
:href="`https://docs.extension.automa.site/reference/workflow-common-errors.html#${item.messageId}`"
target="_blank"
title="About the error"
@click.stop
>
<v-remixicon
name="riArrowLeftLine"
size="20"
class="inline-block text-gray-300"
rotate="135"
/>
</a>
</p>
<router-link
v-if="item.logId"
v-slot="{ navigate }"
:to="{ name: 'logs-details', params: { id: item.logId } }"
custom
>
<v-remixicon
title="Open log detail"
class="ml-2 cursor-pointer text-gray-300"
size="20"
name="riFileTextLine"
@click.stop="navigate"
/>
</router-link>
<router-link
v-if="!isRunning && getBlockPath(item.blockId)"
v-show="currentLog.workflowId && item.blockId"
:to="getBlockPath(item.blockId)"
>
<v-remixicon
name="riExternalLinkLine"
size="20"
title="Go to block"
class="invisible ml-2 cursor-pointer text-gray-300 group-hover:visible"
/>
</router-link>
</div>
<slot name="append-items" />
</div>
</div>
</div>
<div
v-if="currentLog.history.length >= 25"
class="mt-4 lg:flex lg:items-center lg:justify-between"
>
<div class="mb-4 lg:mb-0">
{{ t('components.pagination.text1') }}
<select v-model="pagination.perPage" class="bg-input rounded-md p-1">
<option
v-for="num in [25, 50, 75, 100, 150, 200]"
:key="num"
:value="num"
>
{{ num }}
</option>
</select>
{{
t('components.pagination.text2', {
count: filteredLog.length,
})
}}
</div>
<ui-pagination
v-model="pagination.currentPage"
:per-page="pagination.perPage"
:records="filteredLog.length"
/>
</div>
</div>
<div
v-if="state.itemId && activeLog"
class="dark mb-4 w-full rounded-lg bg-gray-900 text-gray-100 lg:ml-8 lg:mb-0 lg:w-4/12"
>
<div class="relative p-4">
<v-remixicon
name="riCloseLine"
class="absolute top-2 right-2 cursor-pointer text-gray-500"
@click="clearActiveItem"
/>
<table class="ctx-data-table w-full">
<thead>
<tr>
<td class="w-5/12"></td>
<td></td>
</tr>
</thead>
<tbody>
<tr>
<td class="text-gray-300">Name</td>
<td>{{ activeLog.name }}</td>
</tr>
<tr>
<td class="text-gray-300">Description</td>
<td>
<p class="line-clamp leading-tight">
{{ activeLog.description }}
</p>
</td>
</tr>
<tr>
<td class="text-gray-300">Status</td>
<td class="capitalize">{{ activeLog.type }}</td>
</tr>
<tr>
<td class="text-gray-300">Timestamp/Duration</td>
<td>
<span v-if="activeLog.timestamp">
{{ dayjs(activeLog.timestamp).format('DD MMM, HH:mm:ss') }}
/
</span>
{{ countDuration(0, activeLog.duration || 0).trim() }}
</td>
</tr>
<tr v-if="activeLog.message">
<td class="text-gray-300">Message</td>
<td>
<p class="line-clamp leading-tight">
{{ activeLog.message }}
</p>
</td>
</tr>
</tbody>
</table>
</div>
<div class="flex items-center px-4 pb-4">
<p>Log data</p>
<div class="grow" />
<ui-select v-model="state.activeTab">
<option v-for="option in tabs" :key="option.id" :value="option.id">
{{ option.name }}
</option>
</ui-select>
</div>
<div class="log-data-prev px-2 pb-4">
<shared-codemirror
:model-value="logCtxData"
readonly
hide-lang
lang="json"
style="max-height: 460px"
class="scroll"
/>
</div>
</div>
</div>
</template>
<script setup>
/* eslint-disable no-use-before-define */
import dayjs from '@/lib/dayjs';
import { getBlocks } from '@/utils/getSharedData';
import { countDuration, fileSaver } from '@/utils/helper';
import { dataExportTypes, messageHasReferences } from '@/utils/shared';
import objectPath from 'object-path';
import Papa from 'papaparse';
import {
computed,
defineAsyncComponent,
shallowReactive,
shallowRef,
} from 'vue';
import { useI18n } from 'vue-i18n';
const SharedCodemirror = defineAsyncComponent(() =>
import('@/components/newtab/shared/SharedCodemirror.vue')
);
const blocks = getBlocks();
const props = defineProps({
currentLog: {
type: Object,
default: () => ({}),
},
ctxData: {
type: Object,
default: () => ({}),
},
parentLog: {
type: Object,
default: null,
},
isRunning: Boolean,
});
const files = {
'plain-text': {
mime: 'text/plain',
ext: '.txt',
},
json: {
mime: 'application/json',
ext: '.json',
},
csv: {
mime: 'text/csv',
ext: '.csv',
},
};
const logsType = {
success: {
color: 'text-green-400',
icon: 'riCheckLine',
},
stop: {
color: 'text-yellow-400',
icon: 'riStopLine',
},
stopped: {
color: 'text-yellow-400',
icon: 'riStopLine',
},
error: {
color: 'text-red-400',
icon: 'riErrorWarningLine',
},
finish: {
color: 'text-blue-300',
icon: 'riFlagLine',
},
};
const tabs = [
{ id: 'all', name: 'All' },
{ id: 'referenceData.loopData', name: 'Loop data' },
{ id: 'referenceData.variables', name: 'Variables' },
{ id: 'referenceData.prevBlockData', name: 'Previous block data' },
{ id: 'replacedValue', name: 'Replaced value' },
];
const { t, te } = useI18n();
const state = shallowReactive({
itemId: '',
search: '',
activeTab: 'all',
});
const pagination = shallowReactive({
perPage: 25,
currentPage: 1,
});
const activeLog = shallowRef(null);
const translatedLog = computed(() =>
props.currentLog.history.map(translateLog)
);
const filteredLog = computed(() => {
const query = state.search.toLocaleLowerCase();
return translatedLog.value.filter(
(log) =>
log.name.toLocaleLowerCase().includes(query) ||
log.description?.toLocaleLowerCase().includes(query)
);
});
const history = computed(() =>
filteredLog.value.slice(
(pagination.currentPage - 1) * pagination.perPage,
pagination.currentPage * pagination.perPage
)
);
const errorBlock = computed(() => {
if (props.currentLog.status !== 'error') return null;
let block = props.currentLog.history.at(-1);
if (!block || block.type !== 'error' || block.id < 25) return null;
block = translateLog(block);
let { name } = block;
if (block.description) name += ` (${block.description})`;
return {
name,
id: block.id,
message: block.message,
messageId: block.messageId,
};
});
const logCtxData = computed(() => {
let logData = props.ctxData;
if (logData.ctxData) logData = logData.ctxData;
if (!state.itemId || !logData[state.itemId]) return '';
const data = logData[state.itemId];
/* eslint-disable-next-line */
if (data?.referenceData) getDataSnapshot(data.referenceData);
const itemLogData =
state.activeTab === 'all' ? data : objectPath.get(data, state.activeTab);
return JSON.stringify(itemLogData, null, 2);
});
function getDataSnapshot(refData) {
if (!props.ctxData?.dataSnapshot) return;
const data = props.ctxData.dataSnapshot;
const getData = (key) => {
const currentData = refData[key];
if (typeof currentData !== 'string') return currentData;
return data[currentData] ?? {};
};
refData.loopData = getData('loopData');
refData.variables = getData('variables');
}
function exportLogs(type) {
let data = type === 'plain-text' ? '' : [];
const getItemData = {
'plain-text': ([
timestamp,
status,
name,
description,
message,
ctxData,
]) => {
data += `${timestamp} - ${status} - ${name} - ${description} - ${message} - ${JSON.stringify(
ctxData
)} \n`;
},
json: ([timestamp, status, name, description, message, ctxData]) => {
data.push({
timestamp,
status,
name,
description,
message,
data: ctxData,
});
},
csv: (item, index) => {
if (index === 0) {
data.unshift([
'timestamp',
'status',
'name',
'description',
'message',
'data',
]);
}
item[item.length - 1] = JSON.stringify(item[item.length - 1]);
data.push(item);
},
};
translatedLog.value.forEach((item, index) => {
let logData = props.ctxData;
if (logData.ctxData) logData = logData.ctxData;
const itemData = logData[item.id] || null;
if (itemData) getDataSnapshot(itemData.referenceData);
getItemData[type](
[
dayjs(item.timestamp || Date.now()).format('DD-MM-YYYY, hh:mm:ss'),
item.type.toUpperCase(),
item.name,
item.description || 'NULL',
item.message || 'NULL',
itemData,
],
index
);
});
switch (type) {
case 'plain-text':
data = [data];
break;
case 'csv':
data = [Papa.unparse(data)];
data.unshift(new Uint8Array([0xef, 0xbb, 0xbf]));
break;
case 'json':
data = [JSON.stringify(data, null, 2)];
break;
default:
}
const { mime, ext } = files[type];
const blobUrl = URL.createObjectURL(new Blob(data, { type: mime }));
const filename = `[${dayjs().format('DD-MM-YYYY, HH:mm:ss')}] ${
props.currentLog.name
} - logs`;
fileSaver(`${filename}${ext}`, blobUrl);
URL.revokeObjectURL(blobUrl);
}
function clearActiveItem() {
state.itemId = '';
activeLog.value = null;
}
function translateLog(log) {
const copyLog = { ...log };
const getTranslatation = (path, def) => {
const params = typeof path === 'string' ? { path } : path;
return te(params.path) ? t(params.path, params.params) : def;
};
if (['finish', 'stop'].includes(log.type)) {
copyLog.name = t(`log.types.${log.type}`);
} else {
copyLog.name = getTranslatation(
`workflow.blocks.${log.name}.name`,
blocks[log.name].name
);
}
if (copyLog.message && messageHasReferences.includes(copyLog.message)) {
copyLog.messageId = `${copyLog.message}`;
}
copyLog.message = getTranslatation(
{ path: `log.messages.${log.message}`, params: log },
log.message
);
return copyLog;
}
function setActiveLog(item) {
state.itemId = item.id;
activeLog.value = item;
}
function getBlockPath(blockId) {
const { workflowId, teamId } = props.currentLog;
let path = `/workflows/${workflowId}`;
if (workflowId.startsWith('team') && teamId) {
path = `/teams/${teamId}/workflows/${workflowId}`;
}
return `${path}?blockId=${blockId}`;
}
function jumpToError() {
pagination.currentPage = Math.ceil(errorBlock.value.id / pagination.perPage);
const element = document.querySelector('#log-history');
if (!element) return;
element.scrollTo(0, element.scrollHeight);
document.documentElement.scrollTo(0, document.documentElement.scrollHeight);
}
</script>
<style>
.ctx-data-table {
thead td {
padding: 0;
}
td {
@apply p-1;
}
tr {
vertical-align: baseline;
}
}
.log-data-prev .cm-editor {
background-color: transparent;
.cm-activeLine.cm-line {
background-color: rgb(255 255 255 / 0.05) !important;
}
.cm-gutters,
.cm-activeLineGutter,
.cm-gutterElement {
background-color: transparent !important;
}
}
</style>
================================================
FILE: src/components/newtab/logs/LogsTable.vue
================================================
<template>
<div v-if="tableData.body.length === 0" class="text-center">
<img src="@/assets/svg/files-and-folder.svg" class="mx-auto max-w-sm" />
<p class="text-xl font-semibold">{{ t('message.noData') }}</p>
</div>
<template v-else>
<div class="flex items-center">
<ui-tabs
v-model="state.activeTab"
type="fill"
class="mb-4"
color=""
style="padding: 0"
>
<ui-tab value="table"> Table </ui-tab>
<ui-tab value="raw"> Raw </ui-tab>
</ui-tabs>
<div class="grow"></div>
<ui-input
v-if="state.activeTab === 'table'"
v-model="state.query"
:placeholder="t('common.search')"
class="mr-4"
prepend-icon="riSearch2Line"
type="search"
/>
<ui-popover trigger-width>
<template #trigger>
<ui-button variant="accent">
<span>{{ t('log.exportData.title') }}</span>
<v-remixicon name="riArrowDropDownLine" class="ml-2 -mr-1" />
</ui-button>
</template>
<ui-list class="space-y-1">
<ui-list-item
v-for="type in dataExportTypes"
:key="type.id"
v-close-popover
class="cursor-pointer"
@click="exportData(type.id)"
>
{{ t(`log.exportData.types.${type.id}`) }}
</ui-list-item>
</ui-list>
</ui-popover>
</div>
<shared-codemirror
v-show="state.activeTab === 'raw'"
:model-value="JSON.stringify(currentLog.data.table, null, 2)"
readonly
lang="json"
style="max-height: 600px"
/>
<ui-table
v-show="state.activeTab === 'table'"
with-pagination
:headers="tableData.header"
:items="tableData.body"
:search="state.query"
item-key="id"
class="w-full"
/>
</template>
</template>
<script setup>
import { shallowReactive, defineAsyncComponent } from 'vue';
import { useI18n } from 'vue-i18n';
import { dataExportTypes } from '@/utils/shared';
import dataExporter from '@/utils/dataExporter';
const SharedCodemirror = defineAsyncComponent(() =>
import('@/components/newtab/shared/SharedCodemirror.vue')
);
const props = defineProps({
tableData: {
type: Object,
default: () => ({}),
},
currentLog: {
type: Object,
default: () => ({}),
},
});
const { t } = useI18n();
const state = shallowReactive({
query: '',
activeTab: 'table',
});
function exportData(type) {
dataExporter(
props.currentLog.data.table,
{ name: props
gitextract_9m71ul2v/ ├── .babelrc ├── .editorconfig ├── .eslintrc.js ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ └── dependabot.yml ├── .gitignore ├── .prettierrc ├── .vscode/ │ └── settings.json ├── LICENSE.txt ├── README.md ├── business/ │ └── dev/ │ ├── blocks/ │ │ ├── backgroundHandler/ │ │ │ └── index.js │ │ ├── contentHandler/ │ │ │ └── index.js │ │ ├── editComponents/ │ │ │ └── index.js │ │ └── index.js │ ├── index.js │ └── parameters/ │ └── index.js ├── jsconfig.json ├── package.json ├── postcss.config.js ├── secrets.blank.js ├── src/ │ ├── assets/ │ │ └── css/ │ │ ├── drawflow.css │ │ ├── flow.css │ │ ├── fonts.css │ │ ├── style.css │ │ └── tailwind.css │ ├── background/ │ │ ├── BackgroundEventsListeners.js │ │ ├── BackgroundOffscreen.js │ │ ├── BackgroundUtils.js │ │ ├── BackgroundWorkflowTriggers.js │ │ ├── BackgroundWorkflowUtils.js │ │ └── index.js │ ├── common/ │ │ └── utils/ │ │ └── constant.js │ ├── components/ │ │ ├── block/ │ │ │ ├── BlockBase.vue │ │ │ ├── BlockBasic.vue │ │ │ ├── BlockBasicWithFallback.vue │ │ │ ├── BlockConditions.vue │ │ │ ├── BlockDelay.vue │ │ │ ├── BlockElementExists.vue │ │ │ ├── BlockGroup.vue │ │ │ ├── BlockGroup2.vue │ │ │ ├── BlockLoopBreakpoint.vue │ │ │ ├── BlockNote.vue │ │ │ ├── BlockPackage.vue │ │ │ └── BlockRepeatTask.vue │ │ ├── content/ │ │ │ ├── selector/ │ │ │ │ ├── SelectorBlocks.vue │ │ │ │ ├── SelectorElementList.vue │ │ │ │ ├── SelectorElementsDetail.vue │ │ │ │ └── SelectorQuery.vue │ │ │ └── shared/ │ │ │ ├── SharedElementHighlighter.vue │ │ │ └── SharedElementSelector.vue │ │ ├── newtab/ │ │ │ ├── app/ │ │ │ │ ├── AppLogs.vue │ │ │ │ ├── AppLogsItem.vue │ │ │ │ ├── AppLogsItemRunning.vue │ │ │ │ ├── AppLogsItems.vue │ │ │ │ ├── AppSidebar.vue │ │ │ │ └── AppSurvey.vue │ │ │ ├── logs/ │ │ │ │ ├── LogsDataViewer.vue │ │ │ │ ├── LogsFilters.vue │ │ │ │ ├── LogsHistory.vue │ │ │ │ ├── LogsTable.vue │ │ │ │ └── LogsVariables.vue │ │ │ ├── package/ │ │ │ │ ├── PackageDetails.vue │ │ │ │ ├── PackageSettingIOSelect.vue │ │ │ │ └── PackageSettings.vue │ │ │ ├── settings/ │ │ │ │ ├── SettingsBackupItems.vue │ │ │ │ ├── SettingsCloudBackup.vue │ │ │ │ └── jsBlockWrap.js │ │ │ ├── shared/ │ │ │ │ ├── SharedCard.vue │ │ │ │ ├── SharedCodemirror.vue │ │ │ │ ├── SharedConditionBuilder/ │ │ │ │ │ ├── ConditionBuilderInputs.vue │ │ │ │ │ └── index.vue │ │ │ │ ├── SharedElSelectorActions.vue │ │ │ │ ├── SharedLogsTable.vue │ │ │ │ ├── SharedPermissionsModal.vue │ │ │ │ ├── SharedWorkflowState.vue │ │ │ │ ├── SharedWorkflowTriggers.vue │ │ │ │ └── SharedWysiwyg.vue │ │ │ ├── storage/ │ │ │ │ ├── StorageCredentials.vue │ │ │ │ ├── StorageEditTable.vue │ │ │ │ ├── StorageTables.vue │ │ │ │ └── StorageVariables.vue │ │ │ ├── workflow/ │ │ │ │ ├── WorkflowBlockList.vue │ │ │ │ ├── WorkflowDataTable.vue │ │ │ │ ├── WorkflowDetailsCard.vue │ │ │ │ ├── WorkflowEditBlock.vue │ │ │ │ ├── WorkflowEditor.vue │ │ │ │ ├── WorkflowGlobalData.vue │ │ │ │ ├── WorkflowProtect.vue │ │ │ │ ├── WorkflowRunning.vue │ │ │ │ ├── WorkflowSettings.vue │ │ │ │ ├── WorkflowShare.vue │ │ │ │ ├── WorkflowShareTeam.vue │ │ │ │ ├── WorkflowSharedActions.vue │ │ │ │ ├── edit/ │ │ │ │ │ ├── BlockSetting/ │ │ │ │ │ │ ├── BlockSettingGeneral.vue │ │ │ │ │ │ ├── BlockSettingLines.vue │ │ │ │ │ │ └── BlockSettingOnError.vue │ │ │ │ │ ├── EditAiWorkflow.vue │ │ │ │ │ ├── EditAttributeValue.vue │ │ │ │ │ ├── EditAutocomplete.vue │ │ │ │ │ ├── EditBlockSettings.vue │ │ │ │ │ ├── EditBrowserEvent.vue │ │ │ │ │ ├── EditClipboard.vue │ │ │ │ │ ├── EditCloseTab.vue │ │ │ │ │ ├── EditConditions.vue │ │ │ │ │ ├── EditCookie.vue │ │ │ │ │ ├── EditCreateElement.vue │ │ │ │ │ ├── EditDataMapping.vue │ │ │ │ │ ├── EditDelay.vue │ │ │ │ │ ├── EditDeleteData.vue │ │ │ │ │ ├── EditElementExists.vue │ │ │ │ │ ├── EditExecuteWorkflow.vue │ │ │ │ │ ├── EditExportData.vue │ │ │ │ │ ├── EditForms.vue │ │ │ │ │ ├── EditGetText.vue │ │ │ │ │ ├── EditGoogleDrive.vue │ │ │ │ │ ├── EditGoogleSheets.vue │ │ │ │ │ ├── EditGoogleSheetsDrive.vue │ │ │ │ │ ├── EditHandleDialog.vue │ │ │ │ │ ├── EditHandleDownload.vue │ │ │ │ │ ├── EditIncreaseVariable.vue │ │ │ │ │ ├── EditInsertData.vue │ │ │ │ │ ├── EditInteractionBase.vue │ │ │ │ │ ├── EditJavascriptCode.vue │ │ │ │ │ ├── EditLink.vue │ │ │ │ │ ├── EditLogData.vue │ │ │ │ │ ├── EditLoopData.vue │ │ │ │ │ ├── EditLoopElements.vue │ │ │ │ │ ├── EditNewTab.vue │ │ │ │ │ ├── EditNewWindow.vue │ │ │ │ │ ├── EditNotification.vue │ │ │ │ │ ├── EditParameterPrompt.vue │ │ │ │ │ ├── EditPressKey.vue │ │ │ │ │ ├── EditProxy.vue │ │ │ │ │ ├── EditRegexVariable.vue │ │ │ │ │ ├── EditSaveAssets.vue │ │ │ │ │ ├── EditScrollElement.vue │ │ │ │ │ ├── EditSliceVariable.vue │ │ │ │ │ ├── EditSortData.vue │ │ │ │ │ ├── EditSwitchTab.vue │ │ │ │ │ ├── EditSwitchTo.vue │ │ │ │ │ ├── EditTabURL.vue │ │ │ │ │ ├── EditTakeScreenshot.vue │ │ │ │ │ ├── EditTrigger.vue │ │ │ │ │ ├── EditTriggerEvent.vue │ │ │ │ │ ├── EditUploadFile.vue │ │ │ │ │ ├── EditWaitConnections.vue │ │ │ │ │ ├── EditWebhook.vue │ │ │ │ │ ├── EditWhileLoop.vue │ │ │ │ │ ├── EditWorkflowParameters.vue │ │ │ │ │ ├── EditWorkflowState.vue │ │ │ │ │ ├── InsertWorkflowData.vue │ │ │ │ │ ├── Parameter/ │ │ │ │ │ │ ├── ParameterCheckboxValue.vue │ │ │ │ │ │ ├── ParameterInputOptions.vue │ │ │ │ │ │ ├── ParameterInputValue.vue │ │ │ │ │ │ └── ParameterJsonValue.vue │ │ │ │ │ ├── Trigger/ │ │ │ │ │ │ ├── TriggerContextMenu.vue │ │ │ │ │ │ ├── TriggerCronJob.vue │ │ │ │ │ │ ├── TriggerDate.vue │ │ │ │ │ │ ├── TriggerElementChange.vue │ │ │ │ │ │ ├── TriggerElementOptions.vue │ │ │ │ │ │ ├── TriggerInterval.vue │ │ │ │ │ │ ├── TriggerKeyboardShortcut.vue │ │ │ │ │ │ ├── TriggerSpecificDay.vue │ │ │ │ │ │ └── TriggerVisitWeb.vue │ │ │ │ │ └── TriggerEvent/ │ │ │ │ │ ├── TriggerEventInput.vue │ │ │ │ │ ├── TriggerEventKeyboard.vue │ │ │ │ │ ├── TriggerEventMouse.vue │ │ │ │ │ ├── TriggerEventTouch.vue │ │ │ │ │ └── TriggerEventWheel.vue │ │ │ │ ├── editor/ │ │ │ │ │ ├── EditorAddPackage.vue │ │ │ │ │ ├── EditorCustomEdge.vue │ │ │ │ │ ├── EditorDebugging.vue │ │ │ │ │ ├── EditorLocalActions.vue │ │ │ │ │ ├── EditorLocalCtxMenu.vue │ │ │ │ │ ├── EditorLocalSavedBlocks.vue │ │ │ │ │ ├── EditorLogs.vue │ │ │ │ │ ├── EditorPkgActions.vue │ │ │ │ │ ├── EditorSearchBlocks.vue │ │ │ │ │ └── EditorUsedCredentials.vue │ │ │ │ └── settings/ │ │ │ │ ├── SettingsBlocks.vue │ │ │ │ ├── SettingsEvents.vue │ │ │ │ ├── SettingsGeneral.vue │ │ │ │ ├── SettingsTable.vue │ │ │ │ └── event/ │ │ │ │ ├── EventCodeAction.vue │ │ │ │ └── EventCodeHTTP.vue │ │ │ └── workflows/ │ │ │ ├── WorkflowsFolder.vue │ │ │ ├── WorkflowsHosted.vue │ │ │ ├── WorkflowsLocal.vue │ │ │ ├── WorkflowsLocalCard.vue │ │ │ ├── WorkflowsShared.vue │ │ │ └── WorkflowsUserTeam.vue │ │ ├── popup/ │ │ │ └── home/ │ │ │ ├── HomeSelectBlock.vue │ │ │ ├── HomeStartRecording.vue │ │ │ ├── HomeTeamWorkflows.vue │ │ │ └── HomeWorkflowCard.vue │ │ ├── transitions/ │ │ │ ├── TransitionExpand.vue │ │ │ └── TransitionSlide.vue │ │ └── ui/ │ │ ├── UiAutocomplete.vue │ │ ├── UiButton.vue │ │ ├── UiCard.vue │ │ ├── UiCheckbox.vue │ │ ├── UiDialog.vue │ │ ├── UiExpand.vue │ │ ├── UiFileInput.vue │ │ ├── UiImg.vue │ │ ├── UiInput.vue │ │ ├── UiList.vue │ │ ├── UiListItem.vue │ │ ├── UiModal.vue │ │ ├── UiPaginatedSelect.vue │ │ ├── UiPagination.vue │ │ ├── UiPopover.vue │ │ ├── UiRadio.vue │ │ ├── UiSelect.vue │ │ ├── UiSpinner.vue │ │ ├── UiSwitch.vue │ │ ├── UiTab.vue │ │ ├── UiTabPanel.vue │ │ ├── UiTabPanels.vue │ │ ├── UiTable.vue │ │ ├── UiTabs.vue │ │ └── UiTextarea.vue │ ├── composable/ │ │ ├── blockValidation.js │ │ ├── commandManager.js │ │ ├── componentId.js │ │ ├── dialog.js │ │ ├── editorBlock.js │ │ ├── groupTooltip.js │ │ ├── hasPermissions.js │ │ ├── liveQuery.js │ │ ├── shortcut.js │ │ └── theme.js │ ├── content/ │ │ ├── blocksHandler/ │ │ │ ├── handlerAttributeValue.js │ │ │ ├── handlerClipboard.js │ │ │ ├── handlerConditions.js │ │ │ ├── handlerCreateElement.js │ │ │ ├── handlerElementExists.js │ │ │ ├── handlerElementScroll.js │ │ │ ├── handlerEventClick.js │ │ │ ├── handlerForms.js │ │ │ ├── handlerGetText.js │ │ │ ├── handlerHoverElement.js │ │ │ ├── handlerJavascriptCode.js │ │ │ ├── handlerLink.js │ │ │ ├── handlerLoopData.js │ │ │ ├── handlerLoopElements.js │ │ │ ├── handlerPressKey.js │ │ │ ├── handlerSaveAssets.js │ │ │ ├── handlerSwitchTo.js │ │ │ ├── handlerTakeScreenshot.js │ │ │ ├── handlerTriggerEvent.js │ │ │ ├── handlerUploadFile.js │ │ │ └── handlerVerifySelector.js │ │ ├── blocksHandler.js │ │ ├── commandPalette/ │ │ │ ├── App.vue │ │ │ ├── compsUi.js │ │ │ ├── icons.js │ │ │ ├── index.js │ │ │ └── main.js │ │ ├── elementObserver.js │ │ ├── elementSelector/ │ │ │ ├── App.vue │ │ │ ├── compsUi.js │ │ │ ├── generateElementsSelector.js │ │ │ ├── getSelectorOptions.js │ │ │ ├── icons.js │ │ │ ├── index.js │ │ │ ├── listSelector.js │ │ │ ├── main.js │ │ │ ├── selectorFrameContext.js │ │ │ └── vueI18n.js │ │ ├── handleSelector.js │ │ ├── index.js │ │ ├── injectAppStyles.js │ │ ├── services/ │ │ │ ├── recordWorkflow/ │ │ │ │ ├── App.vue │ │ │ │ ├── addBlock.js │ │ │ │ ├── icons.js │ │ │ │ ├── index.js │ │ │ │ ├── main.js │ │ │ │ └── recordEvents.js │ │ │ ├── shortcutListener.js │ │ │ └── webService.js │ │ ├── showExecutedBlock.js │ │ ├── synchronizedLock.js │ │ └── utils.js │ ├── db/ │ │ ├── logs.js │ │ └── storage.js │ ├── directives/ │ │ ├── VAutofocus.js │ │ ├── VClosePopover.js │ │ └── VTooltip.js │ ├── execute/ │ │ ├── index.html │ │ └── index.js │ ├── lib/ │ │ ├── compsUi.js │ │ ├── cronstrue.js │ │ ├── dayjs.js │ │ ├── findSelector.js │ │ ├── mitt.js │ │ ├── pinia.js │ │ ├── query-selector-shadow-dom/ │ │ │ ├── index.js │ │ │ └── normalize.js │ │ ├── tippy.js │ │ ├── tmpl.js │ │ ├── vRemixicon.js │ │ ├── vue-toastification.js │ │ └── vueI18n.js │ ├── locales/ │ │ ├── en/ │ │ │ ├── blocks.json │ │ │ ├── common.json │ │ │ ├── newtab.json │ │ │ └── popup.json │ │ ├── es/ │ │ │ ├── blocks.json │ │ │ ├── common.json │ │ │ ├── newtab.json │ │ │ └── popup.json │ │ ├── fr/ │ │ │ ├── blocks.json │ │ │ ├── common.json │ │ │ ├── newtab.json │ │ │ └── popup.json │ │ ├── it/ │ │ │ ├── blocks.json │ │ │ ├── common.json │ │ │ ├── newtab.json │ │ │ └── popup.json │ │ ├── pt-BR/ │ │ │ ├── blocks.json │ │ │ ├── common.json │ │ │ ├── newtab.json │ │ │ └── popup.json │ │ ├── tr/ │ │ │ ├── blocks.json │ │ │ ├── common.json │ │ │ ├── newtab.json │ │ │ └── popup.json │ │ ├── uk/ │ │ │ ├── blocks.json │ │ │ ├── common.json │ │ │ ├── newtab.json │ │ │ └── popup.json │ │ ├── vi/ │ │ │ ├── blocks.json │ │ │ ├── common.json │ │ │ ├── newtab.json │ │ │ └── popup.json │ │ ├── zh/ │ │ │ ├── blocks.json │ │ │ ├── common.json │ │ │ ├── newtab.json │ │ │ └── popup.json │ │ └── zh-TW/ │ │ ├── blocks.json │ │ ├── common.json │ │ ├── newtab.json │ │ └── popup.json │ ├── manifest.chrome.dev.json │ ├── manifest.chrome.json │ ├── manifest.firefox.json │ ├── newtab/ │ │ ├── App.vue │ │ ├── index.html │ │ ├── index.js │ │ ├── pages/ │ │ │ ├── Packages.vue │ │ │ ├── Recording.vue │ │ │ ├── ScheduledWorkflow.vue │ │ │ ├── Settings.vue │ │ │ ├── Storage.vue │ │ │ ├── Welcome.vue │ │ │ ├── Workflows.vue │ │ │ ├── logs/ │ │ │ │ └── [id].vue │ │ │ ├── settings/ │ │ │ │ ├── SettingsAbout.vue │ │ │ │ ├── SettingsBackup.vue │ │ │ │ ├── SettingsEditor.vue │ │ │ │ ├── SettingsIndex.vue │ │ │ │ ├── SettingsProfile.vue │ │ │ │ └── SettingsShortcuts.vue │ │ │ ├── storage/ │ │ │ │ └── Tables.vue │ │ │ └── workflows/ │ │ │ ├── Host.vue │ │ │ ├── Shared.vue │ │ │ ├── [id].vue │ │ │ └── index.vue │ │ ├── router.js │ │ └── utils/ │ │ ├── RecordWorkflowUtils.js │ │ ├── blocksValidation.js │ │ ├── elementSelector.js │ │ └── startRecordWorkflow.js │ ├── offscreen/ │ │ ├── index.html │ │ ├── index.js │ │ └── message-listener.js │ ├── params/ │ │ ├── App.vue │ │ ├── index.html │ │ └── index.js │ ├── popup/ │ │ ├── App.vue │ │ ├── index.html │ │ ├── index.js │ │ ├── pages/ │ │ │ └── Home.vue │ │ └── router.js │ ├── sandbox/ │ │ ├── index.html │ │ ├── index.js │ │ └── utils/ │ │ ├── handleBlockExpression.js │ │ ├── handleConditionCode.js │ │ └── handleJavascriptBlock.js │ ├── service/ │ │ ├── browser-api/ │ │ │ ├── BrowserAPIEventHandler.js │ │ │ ├── BrowserAPIService.js │ │ │ └── browser-api-map.js │ │ └── renderer/ │ │ └── RendererWorkflowService.js │ ├── stores/ │ │ ├── folder.js │ │ ├── hostedWorkflow.js │ │ ├── main.js │ │ ├── package.js │ │ ├── sharedWorkflow.js │ │ ├── teamWorkflow.js │ │ ├── user.js │ │ └── workflow.js │ ├── utils/ │ │ ├── FindElement.js │ │ ├── USKeyboardLayout.js │ │ ├── api.js │ │ ├── callbackBridge.js │ │ ├── codeEditorAutocomplete.js │ │ ├── compareBlockValue.js │ │ ├── constants/ │ │ │ └── table.js │ │ ├── convertWorkflowData.js │ │ ├── credentialUtil.js │ │ ├── dataExporter.js │ │ ├── dataMigration.js │ │ ├── decryptFlow.js │ │ ├── editor/ │ │ │ ├── DroppedNode.js │ │ │ ├── EditorCommands.js │ │ │ └── editorAutocomplete.js │ │ ├── firstWorkflows.js │ │ ├── getAIPoweredInfo.js │ │ ├── getBlockMessage.js │ │ ├── getFile.js │ │ ├── getSharedData.js │ │ ├── getTranslateLog.js │ │ ├── googleSheetsApi.js │ │ ├── handleFormElement.js │ │ ├── helper.js │ │ ├── message.js │ │ ├── openGDriveFilePicker.js │ │ ├── recordKeys.js │ │ ├── serialization.js │ │ ├── shared.js │ │ ├── simulateEvent/ │ │ │ ├── index.js │ │ │ └── mouseEvent.js │ │ ├── triggerText.js │ │ ├── workflowData.js │ │ └── workflowTrigger.js │ └── workflowEngine/ │ ├── WorkflowEngine.js │ ├── WorkflowLogger.js │ ├── WorkflowManager.js │ ├── WorkflowState.js │ ├── WorkflowWorker.js │ ├── blocksHandler/ │ │ ├── handlerActiveTab.js │ │ ├── handlerAiWorkflow.js │ │ ├── handlerBlockPackage.js │ │ ├── handlerBlocksGroup.js │ │ ├── handlerBrowserEvent.js │ │ ├── handlerClipboard.js │ │ ├── handlerCloseTab.js │ │ ├── handlerConditions.js │ │ ├── handlerCookie.js │ │ ├── handlerCreateElement.js │ │ ├── handlerDataMapping.js │ │ ├── handlerDelay.js │ │ ├── handlerDeleteData.js │ │ ├── handlerElementExists.js │ │ ├── handlerExecuteWorkflow.js │ │ ├── handlerExportData.js │ │ ├── handlerForwardPage.js │ │ ├── handlerGoBack.js │ │ ├── handlerGoogleDrive.js │ │ ├── handlerGoogleSheets.js │ │ ├── handlerGoogleSheetsDrive.js │ │ ├── handlerHandleDialog.js │ │ ├── handlerHandleDownload.js │ │ ├── handlerHoverElement.js │ │ ├── handlerIncreaseVariable.js │ │ ├── handlerInsertData.js │ │ ├── handlerInteractionBlock.js │ │ ├── handlerJavascriptCode.js │ │ ├── handlerLink.js │ │ ├── handlerLogData.js │ │ ├── handlerLoopBreakpoint.js │ │ ├── handlerLoopData.js │ │ ├── handlerLoopElements.js │ │ ├── handlerNewTab.js │ │ ├── handlerNewWindow.js │ │ ├── handlerNotification.js │ │ ├── handlerParameterPrompt.js │ │ ├── handlerProxy.js │ │ ├── handlerRegexVariable.js │ │ ├── handlerReloadTab.js │ │ ├── handlerRepeatTask.js │ │ ├── handlerSaveAssets.js │ │ ├── handlerSliceVariable.js │ │ ├── handlerSortData.js │ │ ├── handlerSwitchTab.js │ │ ├── handlerSwitchTo.js │ │ ├── handlerTabUrl.js │ │ ├── handlerTakeScreenshot.js │ │ ├── handlerTrigger.js │ │ ├── handlerWaitConnections.js │ │ ├── handlerWebhook.js │ │ ├── handlerWhileLoop.js │ │ └── handlerWorkflowState.js │ ├── blocksHandler.js │ ├── helper.js │ ├── injectContentScript.js │ ├── templating/ │ │ ├── index.js │ │ ├── mustacheReplacer.js │ │ ├── renderString.js │ │ └── templatingFunctions.js │ ├── utils/ │ │ ├── conditionCode.js │ │ ├── javascriptBlockUtil.js │ │ ├── testConditions.js │ │ └── webhookUtil.js │ └── workflowEvent.js ├── tailwind.config.js ├── utils/ │ ├── build-zip.js │ ├── build.js │ ├── clean-build-cache.js │ ├── env.js │ └── webserver.js └── webpack.config.js
SYMBOL INDEX (550 symbols across 166 files)
FILE: src/background/BackgroundEventsListeners.js
function handleScheduleBackup (line 9) | async function handleScheduleBackup() {
class BackgroundEventsListeners (line 78) | class BackgroundEventsListeners {
method onActionClicked (line 79) | static onActionClicked() {
method onCommand (line 83) | static onCommand(name) {
method onAlarms (line 91) | static onAlarms(event) {
method onWebNavigationCompleted (line 100) | static onWebNavigationCompleted({ tabId, url, frameId }) {
method onContextMenuClicked (line 106) | static onContextMenuClicked(event, tab) {
method onNotificationClicked (line 110) | static async onNotificationClicked(notificationId) {
method onRuntimeStartup (line 123) | static onRuntimeStartup() {
method onHistoryStateUpdated (line 129) | static onHistoryStateUpdated({ frameId, url, tabId }) {
method onRuntimeInstalled (line 135) | static async onRuntimeInstalled({ reason }) {
FILE: src/background/BackgroundOffscreen.js
constant OFFSCREEN_URL (line 7) | const OFFSCREEN_URL = Browser.runtime.getURL('/offscreen.html');
class BackgroundOffscreen (line 9) | class BackgroundOffscreen {
method instance (line 17) | static get instance() {
method constructor (line 28) | constructor() {
method #ensureDocument (line 38) | async #ensureDocument() {
method isOpened (line 61) | async isOpened() {
method sendMessage (line 78) | async sendMessage(name, data) {
FILE: src/background/BackgroundUtils.js
class BackgroundUtils (line 4) | class BackgroundUtils {
method openDashboard (line 5) | static async openDashboard(url, updateTab = true) {
method sendMessageToDashboard (line 50) | static async sendMessageToDashboard(type, data) {
FILE: src/background/BackgroundWorkflowTriggers.js
class BackgroundWorkflowTriggers (line 11) | class BackgroundWorkflowTriggers {
method visitWebTriggers (line 12) | static async visitWebTriggers(tabId, tabUrl, spa = false) {
method scheduleWorkflow (line 44) | static async scheduleWorkflow({ name }) {
method contextMenu (line 123) | static async contextMenu({ parentMenuItemId, menuItemId, frameId }, ta...
method reRegisterTriggers (line 153) | static async reRegisterTriggers(isStartup = false) {
FILE: src/background/BackgroundWorkflowUtils.js
class BackgroundWorkflowUtils (line 5) | class BackgroundWorkflowUtils {
method instance (line 13) | static get instance() {
method constructor (line 22) | constructor() {
method flattenTeamWorkflows (line 26) | static flattenTeamWorkflows(workflows) {
method getWorkflow (line 30) | static async getWorkflow(workflowId) {
method #ensureWorkflowManager (line 63) | async #ensureWorkflowManager() {
method stopExecution (line 76) | async stopExecution(stateId) {
method resumeExecution (line 92) | async resumeExecution(stateId, nextBlock) {
method updateExecutionState (line 111) | async updateExecutionState(stateId, data) {
method executeWorkflow (line 124) | async executeWorkflow(workflowData, options) {
FILE: src/background/index.js
function executeCommands (line 107) | async function executeCommands() {
constant DOWNLOADS_STORAGE_KEY (line 734) | const DOWNLOADS_STORAGE_KEY = 'automa-rename-downloaded-files';
function determineFilenameListener (line 753) | function determineFilenameListener(item, suggest) {
function handleDownloadChanged (line 830) | function handleDownloadChanged(downloadDelta) {
function handleDownloadCreated (line 888) | async function handleDownloadCreated(downloadItem) {
function cleanupDownloadListeners (line 929) | function cleanupDownloadListeners() {
function registerBackgroundDownloadListeners (line 944) | async function registerBackgroundDownloadListeners() {
FILE: src/common/utils/constant.js
constant IS_FIREFOX (line 1) | const IS_FIREFOX = BROWSER_TYPE === 'firefox';
FILE: src/composable/blockValidation.js
function useBlockValidation (line 4) | function useBlockValidation(blockId, data) {
FILE: src/composable/commandManager.js
function useCommandManager (line 3) | function useCommandManager({ maxHistory = 100 } = {}) {
FILE: src/composable/componentId.js
function useComponentId (line 3) | function useComponentId(prefix) {
FILE: src/composable/dialog.js
function useDialog (line 3) | function useDialog() {
FILE: src/composable/editorBlock.js
function useEditorBlock (line 5) | function useEditorBlock(label) {
FILE: src/composable/groupTooltip.js
function useGroupTooltip (line 5) | function useGroupTooltip(elements, options = {}) {
FILE: src/composable/hasPermissions.js
function useHasPermissions (line 6) | function useHasPermissions(permissions) {
FILE: src/composable/liveQuery.js
function useLiveQuery (line 4) | function useLiveQuery(querier) {
FILE: src/composable/shortcut.js
function getReadableShortcut (line 65) | function getReadableShortcut(str) {
function getShortcut (line 84) | function getShortcut(id, data) {
function useShortcut (line 95) | function useShortcut(shortcuts, handler) {
FILE: src/composable/theme.js
function useTheme (line 12) | function useTheme() {
FILE: src/content/blocksHandler/handlerAttributeValue.js
function handleAttributeValue (line 3) | function handleAttributeValue(block) {
FILE: src/content/blocksHandler/handlerClipboard.js
function clipboard (line 1) | function clipboard() {
FILE: src/content/blocksHandler/handlerConditions.js
function handleConditionElement (line 8) | async function handleConditionElement({ data, type, id, frameSelector }) {
function handleConditionCode (line 56) | async function handleConditionCode({ data, refData }) {
FILE: src/content/blocksHandler/handlerCreateElement.js
function createNode (line 10) | function createNode(tag, attrs = {}, content = '') {
function createElement (line 21) | async function createElement(block) {
FILE: src/content/blocksHandler/handlerElementExists.js
function elementExists (line 3) | function elementExists(block) {
FILE: src/content/blocksHandler/handlerElementScroll.js
function isElScrollable (line 3) | function isElScrollable(element) {
function findScrollableElement (line 14) | function findScrollableElement(
function elementScroll (line 50) | function elementScroll(block) {
FILE: src/content/blocksHandler/handlerEventClick.js
function eventClick (line 6) | function eventClick(block) {
FILE: src/content/blocksHandler/handlerForms.js
function forms (line 7) | async function forms(block) {
FILE: src/content/blocksHandler/handlerGetText.js
function getText (line 3) | function getText(block) {
FILE: src/content/blocksHandler/handlerHoverElement.js
function eventClick (line 5) | function eventClick(block) {
FILE: src/content/blocksHandler/handlerJavascriptCode.js
function javascriptCode (line 4) | function javascriptCode({ data, isPreloadScripts, frameSelector }) {
FILE: src/content/blocksHandler/handlerLink.js
function link (line 3) | async function link(block) {
FILE: src/content/blocksHandler/handlerLoopData.js
function loopElements (line 5) | async function loopElements(block) {
FILE: src/content/blocksHandler/handlerLoopElements.js
function getScrollParent (line 5) | function getScrollParent(node) {
function excludeSelector (line 23) | function excludeSelector({ type, selector, loopAttr }) {
FILE: src/content/blocksHandler/handlerPressKey.js
function pressKeyWithJs (line 14) | async function pressKeyWithJs({ element, keys, pressTime }) {
function pressKeyWithCommand (line 94) | async function pressKeyWithCommand({
function pressKey (line 156) | async function pressKey({ data, debugMode, activeTabId }) {
FILE: src/content/blocksHandler/handlerSaveAssets.js
function saveAssets (line 3) | async function saveAssets(block) {
FILE: src/content/blocksHandler/handlerSwitchTo.js
function switchTo (line 6) | function switchTo(block) {
FILE: src/content/blocksHandler/handlerTakeScreenshot.js
function findScrollableElement (line 5) | function findScrollableElement(
function injectStyle (line 34) | function injectStyle() {
function canvasToBase64 (line 43) | function canvasToBase64(canvas, { format, quality }) {
function loadAsyncImg (line 46) | function loadAsyncImg(src) {
function takeScreenshot (line 55) | async function takeScreenshot(tabId, options) {
function captureElement (line 65) | async function captureElement({ selector, tabId, options, $frameRect }) {
FILE: src/content/blocksHandler/handlerTriggerEvent.js
function triggerEvent (line 66) | function triggerEvent({ data, id, frameSelector, debugMode, activeTabId ...
FILE: src/content/blocksHandler/handlerUploadFile.js
function injectFiles (line 4) | function injectFiles(element, files) {
FILE: src/content/blocksHandler/handlerVerifySelector.js
constant SLEEP_TIME (line 4) | const SLEEP_TIME = 1700;
function verifySelector (line 6) | async function verifySelector(block) {
FILE: src/content/commandPalette/index.js
function pageLoaded (line 5) | function pageLoaded() {
FILE: src/content/elementObserver.js
function matchPatternToRegex (line 34) | function matchPatternToRegex(str) {
function tryObserve (line 44) | function tryObserve({ selector, observer, options, id }) {
FILE: src/content/elementSelector/listSelector.js
function getAllSiblings (line 4) | function getAllSiblings(el, selector) {
function getCssPath (line 37) | function getCssPath(el, root = document.body) {
function getElementList (line 68) | function getElementList(el, maxDepth = 50, paths = []) {
FILE: src/content/elementSelector/selectorFrameContext.js
function getElementRectWithOffset (line 10) | function getElementRectWithOffset(element, data) {
function getElementsRect (line 19) | function getElementsRect(data) {
function resetElementSelector (line 71) | function resetElementSelector(data) {
function findElement (line 82) | function findElement({ selector, selectorType, frameRect }) {
function onMessage (line 105) | function onMessage({ data }) {
FILE: src/content/handleSelector.js
function markElement (line 6) | function markElement(el, { id, data }) {
function getDocumentCtx (line 12) | function getDocumentCtx(frameSelector) {
function queryElements (line 29) | function queryElements(data, documentCtx = document) {
FILE: src/content/index.js
function messageToFrame (line 21) | function messageToFrame(frameElement, blockData) {
function executeBlock (line 52) | async function executeBlock(data) {
function messageListener (line 121) | async function messageListener({ data, source }) {
FILE: src/content/injectAppStyles.js
function generateStyleEl (line 3) | function generateStyleEl(css, classes = true) {
FILE: src/content/services/recordWorkflow/recordEvents.js
function addBlock (line 15) | async function addBlock(detail) {
function onChange (line 42) | function onChange({ target }) {
function onKeydown (line 114) | async function onKeydown(event) {
function onClick (line 176) | function onClick(event) {
function onFocusIn (line 319) | function onFocusIn({ target }) {
function onFocusOut (line 325) | function onFocusOut({ target }) {
function cleanUp (line 331) | function cleanUp() {
FILE: src/content/services/shortcutListener.js
function automaCustomEventListener (line 9) | function automaCustomEventListener(findWorkflow) {
function workflowShortcutsListener (line 27) | function workflowShortcutsListener(findWorkflow, shortcutsObj) {
function getWorkflows (line 65) | async function getWorkflows() {
FILE: src/content/services/webService.js
function initWebListener (line 8) | function initWebListener() {
function sendMessageBack (line 25) | function sendMessageBack(type, payload = {}) {
method upgrade (line 42) | upgrade(event) {
FILE: src/content/showExecutedBlock.js
function generateElement (line 3) | function generateElement(block) {
FILE: src/content/synchronizedLock.js
class SynchronizedLock (line 1) | class SynchronizedLock {
method constructor (line 2) | constructor() {
method getLock (line 7) | async getLock(timeout = 10000) {
method releaseLock (line 25) | releaseLock() {
FILE: src/content/utils.js
function simulateClickElement (line 1) | function simulateClickElement(element) {
function generateLoopSelectors (line 16) | function generateLoopSelectors(
function elementSelectorInstance (line 40) | function elementSelectorInstance() {
function getElementRect (line 54) | function getElementRect(target, withAttributes) {
function getElementPath (line 81) | function getElementPath(el, root = document.documentElement) {
function generateXPath (line 92) | function generateXPath(element, root = document.body) {
function automaRefDataStr (line 118) | function automaRefDataStr(varName) {
function messageTopFrame (line 153) | function messageTopFrame(windowCtx) {
function getElementPosition (line 174) | async function getElementPosition(element) {
FILE: src/directives/VAutofocus.js
method mounted (line 2) | mounted(el, { value = true }) {
FILE: src/directives/VClosePopover.js
method mounted (line 4) | mounted(el) {
method beforeUnmount (line 7) | beforeUnmount(el) {
FILE: src/directives/VTooltip.js
function getContent (line 3) | function getContent(content) {
method mounted (line 16) | mounted(el, { value, arg = 'top', instance, modifiers }) {
method updated (line 31) | updated(el, { value, arg = 'top' }) {
FILE: src/execute/index.js
function getWorkflowDetail (line 5) | function getWorkflowDetail() {
function writeResult (line 24) | function writeResult(text) {
FILE: src/lib/compsUi.js
function componentsExtractor (line 12) | function componentsExtractor(app, components) {
FILE: src/lib/cronstrue.js
function readableCron (line 12) | function readableCron(expression) {
FILE: src/lib/pinia.js
function saveToStoragePlugin (line 4) | function saveToStoragePlugin({ store, options }) {
FILE: src/lib/query-selector-shadow-dom/index.js
function querySelectorAllDeep (line 22) | function querySelectorAllDeep(
function querySelectorDeep (line 30) | function querySelectorDeep(
function _querySelectorDeep (line 38) | function _querySelectorDeep(selector, findMany, root, allElements = null) {
function findMatchingElement (line 101) | function findMatchingElement(splitSelector, possibleElementsIndex, root) {
function splitByCharacterUnlessQuoted (line 137) | function splitByCharacterUnlessQuoted(selector, character) {
function isDocumentNode (line 162) | function isDocumentNode(node) {
function findParentOrHost (line 169) | function findParentOrHost(element, root) {
function collectAllElementsDeep (line 193) | function collectAllElementsDeep(
FILE: src/lib/query-selector-shadow-dom/normalize.js
function normalizeSelector (line 15) | function normalizeSelector(sel) {
FILE: src/lib/vRemixicon.js
method setup (line 341) | setup(props) {
method install (line 387) | install(app) {
FILE: src/lib/vueI18n.js
function setI18nLanguage (line 11) | function setI18nLanguage(locale) {
function loadLocaleMessages (line 17) | async function loadLocaleMessages(locale, location) {
FILE: src/newtab/utils/RecordWorkflowUtils.js
class RecordWorkflowUtils (line 6) | class RecordWorkflowUtils {
method updateRecording (line 7) | static async updateRecording(callback) {
method onTabCreated (line 20) | static onTabCreated(tab) {
method onTabsActivated (line 50) | static async onTabsActivated({ tabId }) {
method onWebNavigationCommited (line 69) | static onWebNavigationCommited({ frameId, tabId, url, transitionType }) {
method onWebNavigationCompleted (line 103) | static async onWebNavigationCompleted({ tabId, url, frameId }) {
FILE: src/newtab/utils/blocksValidation.js
function validateTrigger (line 11) | async function validateTrigger(data) {
function validateExecuteWorkflow (line 73) | async function validateExecuteWorkflow(data) {
function validateNewTab (line 79) | async function validateNewTab(data) {
function validateSwitchTab (line 85) | async function validateSwitchTab(data) {
function validateProxy (line 102) | async function validateProxy(data) {
function validateCloseTab (line 108) | async function validateCloseTab(data) {
function validateTakeScreenshot (line 116) | async function validateTakeScreenshot(data) {
function validateInteractionBasic (line 124) | async function validateInteractionBasic(data) {
function validateExportData (line 130) | async function validateExportData(data) {
function validateAttributeValue (line 148) | async function validateAttributeValue(data) {
function validateGoogleSheets (line 158) | async function validateGoogleSheets(data) {
function validateWebhook (line 168) | async function validateWebhook(data) {
function validateLoopData (line 174) | async function validateLoopData(data) {
function validateLoopElements (line 194) | async function validateLoopElements(data) {
function validateClipboard (line 209) | async function validateClipboard() {
function validateSwitchTo (line 223) | async function validateSwitchTo(data) {
function validateUploadFile (line 231) | async function validateUploadFile(data) {
function validateSaveAssets (line 242) | async function validateSaveAssets(data) {
function validatePressKey (line 256) | async function validatePressKey(data) {
function validateNotification (line 269) | async function validateNotification() {
function validateCookie (line 276) | async function validateCookie() {
FILE: src/newtab/utils/elementSelector.js
function makeDashboardFocus (line 6) | async function makeDashboardFocus() {
function initElementSelector (line 16) | async function initElementSelector(tab = null) {
function verifySelector (line 49) | async function verifySelector(data) {
function selectElement (line 81) | async function selectElement(name) {
FILE: src/sandbox/index.js
function fetchResponse (line 8) | function fetchResponse({ id, data }) {
function sendResponse (line 26) | function sendResponse(payload) {
FILE: src/sandbox/utils/handleJavascriptBlock.js
function cleanUp (line 74) | function cleanUp() {
FILE: src/service/browser-api/BrowserAPIEventHandler.js
constant BROWSER_API_EVENTS (line 6) | const BROWSER_API_EVENTS = {
function onBrowserAPIEvent (line 11) | function onBrowserAPIEvent(name, ...args) {
class BrowserAPIEventHandler (line 23) | class BrowserAPIEventHandler {
method instance (line 31) | static get instance() {
method constructor (line 51) | constructor() {
method createEventListener (line 63) | createEventListener(name) {
method onBrowserEventListener (line 140) | onBrowserEventListener(event) {
method onToggleBrowserEventListener (line 149) | onToggleBrowserEventListener({ name, type }) {
FILE: src/service/browser-api/BrowserAPIService.js
constant IS_BROWSER_API_AVAILABLE (line 21) | const IS_BROWSER_API_AVAILABLE = 'tabs' in Browser;
function sendBrowserApiMessage (line 23) | function sendBrowserApiMessage(name, ...args) {
class BrowserContentScript (line 36) | class BrowserContentScript {
method isContentScriptInjected (line 42) | static async isContentScriptInjected(target, messageId) {
method inject (line 75) | static async inject({ file, target, injectImmediately, waitUntilInject...
method isInjected (line 146) | static async isInjected({ tabId, allFrames, frameId }, messageId) {
class BrowserAPIService (line 165) | class BrowserAPIService {
method runtimeMessageHandler (line 170) | static runtimeMessageHandler({ args, name }) {
FILE: src/service/renderer/RendererWorkflowService.js
class RendererWorkflowService (line 4) | class RendererWorkflowService {
method executeWorkflow (line 5) | static executeWorkflow(workflowData, options) {
method stopWorkflowExecution (line 22) | static stopWorkflowExecution(executionId) {
FILE: src/stores/folder.js
method addFolder (line 14) | async addFolder(name) {
method deleteFolder (line 24) | async deleteFolder(id) {
method updateFolder (line 33) | async updateFolder(id, data = {}) {
method load (line 42) | load() {
FILE: src/stores/hostedWorkflow.js
method loadData (line 23) | async loadData() {
method insert (line 30) | async insert(data, idKey = 'hostId') {
method delete (line 43) | async delete(id) {
method update (line 51) | async update({ id, data }) {
method fetchWorkflows (line 59) | async fetchWorkflows(ids) {
FILE: src/stores/main.js
method loadSettings (line 43) | loadSettings() {
method updateSettings (line 49) | async updateSettings(settings = {}) {
method checkGDriveIntegration (line 53) | async checkGDriveIntegration(force = false, retryCount = 0) {
method getConnectedSheets (line 91) | async getConnectedSheets() {
FILE: src/stores/package.js
method insert (line 43) | async insert(data, newId = true) {
method update (line 54) | async update({ id, data }) {
method delete (line 63) | async delete(id) {
method deleteShared (line 74) | deleteShared(id) {
method insertShared (line 78) | insertShared(id) {
method loadData (line 81) | async loadData(force = false) {
method loadShared (line 91) | async loadShared() {
FILE: src/stores/sharedWorkflow.js
method insert (line 18) | insert(data) {
method update (line 27) | update({ id, data }) {
method delete (line 34) | delete(id) {
method fetchWorkflows (line 37) | async fetchWorkflows(useCache = true) {
FILE: src/stores/teamWorkflow.js
method insert (line 28) | async insert(teamId, data) {
method update (line 41) | async update({ teamId, id, data, deepmerge = false }) {
method delete (line 58) | async delete(teamId, id) {
method loadData (line 66) | async loadData() {
FILE: src/stores/user.js
method loadUser (line 27) | async loadUser(options = false) {
method signOut (line 83) | async signOut() {
FILE: src/stores/workflow.js
function convertWorkflowsToObject (line 82) | function convertWorkflowsToObject(workflows) {
method loadData (line 115) | async loadData() {
method updateStates (line 138) | updateStates(newStates) {
method insert (line 141) | async insert(data = {}, options = {}) {
method update (line 168) | async update({ id, data = {}, deep = false }) {
method insertOrUpdate (line 215) | async insertOrUpdate(
method delete (line 247) | async delete(id) {
FILE: src/utils/FindElement.js
class FindElement (line 21) | class FindElement {
method cssSelector (line 22) | static cssSelector(data, documentCtx = document) {
method xpath (line 54) | static xpath(data, documentCtx = document) {
FILE: src/utils/api.js
function fetchApi (line 5) | async function fetchApi(path, options = {}) {
function cacheApi (line 44) | async function cacheApi(key, callback, useCache = true) {
function getSharedWorkflows (line 79) | async function getSharedWorkflows(useCache = true) {
function getUserWorkflows (line 110) | async function getUserWorkflows(useCache = true) {
function validateOauthToken (line 158) | function validateOauthToken() {
function fetchGapi (line 199) | async function fetchGapi(url, resource = {}, options = {}) {
FILE: src/utils/callbackBridge.js
function isCallbackBridge (line 8) | function isCallbackBridge(obj) {
class CallbackBridge (line 16) | class CallbackBridge {
method constructor (line 17) | constructor() {
method setupMessageHandlers (line 22) | setupMessageHandlers() {
method createCallbackHandler (line 49) | createCallbackHandler(callback, callbackId) {
method executeCallback (line 89) | static async executeCallback(callbackId, result, error = null) {
method generateCallbackId (line 101) | static generateCallbackId() {
function processCallbacksInData (line 115) | function processCallbacksInData(obj, bridge) {
function sendMessageWithCallbacks (line 143) | async function sendMessageWithCallbacks(messageName, data, prefix = '') {
function executeCallbacksInData (line 154) | async function executeCallbacksInData(obj, result, error = null) {
function hasCallbackBridges (line 181) | function hasCallbackBridges(obj) {
function getCallbackBridgeIds (line 202) | function getCallbackBridgeIds(obj) {
FILE: src/utils/codeEditorAutocomplete.js
function completeProperties (line 8) | function completeProperties(from, object) {
function completeFromGlobalScope (line 37) | function completeFromGlobalScope(context) {
function automaFuncsCompletion (line 59) | function automaFuncsCompletion(snippets) {
FILE: src/utils/convertWorkflowData.js
function extractBlock (line 21) | function extractBlock(blockId) {
FILE: src/utils/credentialUtil.js
function encryptValue (line 8) | function encryptValue(value) {
function decryptValue (line 16) | function decryptValue(value) {
FILE: src/utils/dataExporter.js
function generateJSON (line 19) | function generateJSON(keys, data) {
FILE: src/utils/decryptFlow.js
function getWorkflowPass (line 8) | function getWorkflowPass(pass) {
FILE: src/utils/editor/DroppedNode.js
class DroppedNode (line 7) | class DroppedNode {
method isNode (line 8) | static isNode(target) {
method isHandle (line 14) | static isHandle(target) {
method isEdge (line 18) | static isEdge(target) {
method replaceNode (line 22) | static replaceNode(editor, { block, target: targetEl }) {
method appendNode (line 83) | static appendNode(editor, { target, nodeId }) {
method insertBetweenNode (line 97) | static insertBetweenNode(editor, { target, nodeId, outputs }) {
FILE: src/utils/editor/EditorCommands.js
class EditorCommands (line 1) | class EditorCommands {
method constructor (line 2) | constructor(editor, initialStates = {}) {
method nodeAdded (line 7) | nodeAdded(addedNodes) {
method nodeRemoved (line 25) | nodeRemoved(ids) {
method edgeAdded (line 38) | edgeAdded(addedEdges) {
method edgeRemoved (line 56) | edgeRemoved(ids) {
FILE: src/utils/editor/editorAutocomplete.js
function getData (line 10) | function getData(blockName, blockData) {
method trigger (line 29) | trigger(blockId, data) {
FILE: src/utils/getAIPoweredInfo.js
constant BASE_URL (line 3) | const BASE_URL = secrets.apApiUrl;
FILE: src/utils/getFile.js
function readFileAsBase64 (line 1) | function readFileAsBase64(blob) {
function downloadFile (line 11) | async function downloadFile(url, options) {
function getLocalFile (line 29) | function getLocalFile(path, options) {
FILE: src/utils/getSharedData.js
function getBlocks (line 4) | function getBlocks() {
FILE: src/utils/getTranslateLog.js
function translateLog (line 14) | function translateLog(log) {
function getDataSnapshot (line 45) | function getDataSnapshot(propsCtxData, refData) {
function getLogs (line 66) | function getLogs(dataType, translatedLog, curStateCtxData) {
FILE: src/utils/googleSheetsApi.js
function queryBuilder (line 3) | function queryBuilder(obj) {
method getUrl (line 16) | getUrl(path) {
method getValues (line 19) | getValues({ spreadsheetId, range }) {
method getRange (line 24) | getRange({ spreadsheetId, range }) {
method clearValues (line 33) | clearValues({ spreadsheetId, range }) {
method updateValues (line 40) | updateValues({ spreadsheetId, range, options, append }) {
method create (line 59) | create(name) {
method addSheet (line 71) | addSheet({ sheetName, spreadsheetId }) {
method getUrl (line 89) | getUrl(spreadsheetId, range) {
method getValues (line 92) | getValues({ spreadsheetId, range }) {
method getRange (line 97) | getRange({ spreadsheetId, range }) {
method clearValues (line 112) | clearValues({ spreadsheetId, range }) {
method updateValues (line 117) | updateValues({ spreadsheetId, range, options = {}, append }) {
FILE: src/utils/handleFormElement.js
function reactJsEvent (line 9) | function reactJsEvent(element, value) {
function formEvent (line 17) | function formEvent(element, data) {
function inputText (line 62) | async function inputText({ data, element, isEditable }) {
FILE: src/utils/helper.js
function getActiveTab (line 4) | async function getActiveTab() {
function isXPath (line 29) | function isXPath(str) {
function visibleInViewport (line 35) | function visibleInViewport(element) {
function sleep (line 49) | function sleep(timeout = 500) {
function findTriggerBlock (line 57) | function findTriggerBlock(drawflow = {}) {
function throttle (line 73) | function throttle(callback, limit) {
function convertArrObjTo2DArr (line 87) | function convertArrObjTo2DArr(arr) {
function convert2DArrayToArrayObj (line 113) | function convert2DArrayToArrayObj(values) {
function parseJSON (line 143) | function parseJSON(data, def) {
function parseFlow (line 153) | function parseFlow(flow) {
function replaceMustache (line 159) | function replaceMustache(str, replacer) {
function openFilePicker (line 164) | function openFilePicker(acceptedFileTypes = [], attrs = {}) {
function fileSaver (line 193) | function fileSaver(filename, data) {
function countDuration (line 202) | function countDuration(started, ended) {
function toCamelCase (line 212) | function toCamelCase(str, capitalize = false) {
function isObject (line 222) | function isObject(obj) {
function objectHasKey (line 226) | function objectHasKey(obj, key) {
function isWhitespace (line 230) | function isWhitespace(str) {
function debounce (line 234) | function debounce(callback, time = 200) {
function clearCache (line 251) | async function clearCache(workflow) {
function arraySorter (line 273) | function arraySorter({ data, key, order = 'asc' }) {
FILE: src/utils/message.js
function sendMessage (line 14) | function sendMessage(name = '', data = {}, prefix = '') {
class MessageListener (line 27) | class MessageListener {
method constructor (line 30) | constructor(prefix = '') {
method on (line 37) | on(name, listener) {
method listener (line 48) | listener(message, sender) {
method sendMessage (line 88) | sendMessage(name, data) {
FILE: src/utils/openGDriveFilePicker.js
function fetchGDriveSheets (line 8) | async function fetchGDriveSheets() {
function openGDrivePickerPopup (line 43) | function openGDrivePickerPopup(accessToken) {
FILE: src/utils/recordKeys.js
function recordPressedKey (line 4) | function recordPressedKey(
function recordShortcut (line 36) | function recordShortcut(
FILE: src/utils/serialization.js
function serializeFunctions (line 1) | function serializeFunctions(obj) {
function deserializeFunctions (line 26) | function deserializeFunctions(obj) {
FILE: src/utils/simulateEvent/index.js
function getEventObj (line 3) | function getEventObj(name, params) {
FILE: src/utils/simulateEvent/mouseEvent.js
function mousedown (line 2) | async function mousedown() {
function mouseup (line 6) | async function mouseup() {
function click (line 10) | async function click() {
function dblclick (line 16) | async function dblclick() {
function mousemove (line 20) | async function mousemove() {
function mouseenter (line 24) | async function mouseenter() {
function mouseleave (line 27) | async function mouseleave() {
FILE: src/utils/workflowData.js
method hasPermission (line 18) | hasPermission({ data }) {
method hasPermission (line 36) | hasPermission() {
method hasPermission (line 46) | hasPermission() {
method hasPermission (line 52) | hasPermission() {
method hasPermission (line 58) | hasPermission() {
method hasPermission (line 64) | hasPermission() {
function getWorkflowPermissions (line 70) | async function getWorkflowPermissions(drawflow) {
function importWorkflow (line 95) | function importWorkflow(attrs = {}) {
function convertWorkflow (line 177) | function convertWorkflow(workflow, additionalKeys = []) {
function findIncludedWorkflows (line 201) | function findIncludedWorkflows(
function exportWorkflow (line 235) | function exportWorkflow(workflow) {
FILE: src/utils/workflowTrigger.js
function registerContextMenu (line 6) | function registerContextMenu(triggerId, data) {
function removeFromWorkflowQueue (line 71) | async function removeFromWorkflowQueue(workflowId) {
function cleanWorkflowTriggers (line 84) | async function cleanWorkflowTriggers(workflowId, triggers) {
function registerSpecificDay (line 150) | function registerSpecificDay(workflowId, data) {
function registerInterval (line 187) | function registerInterval(workflowId, data) {
function registerSpecificDate (line 197) | async function registerSpecificDate(workflowId, data) {
function registerVisitWeb (line 216) | async function registerVisitWeb(workflowId, data) {
function registerKeyboardShortcut (line 244) | async function registerKeyboardShortcut(workflowId, data) {
function registerOnStartup (line 257) | async function registerOnStartup() {
function registerCronJob (line 261) | async function registerCronJob(workflowId, data) {
function registerWorkflowTrigger (line 283) | async function registerWorkflowTrigger(workflowId, { data }) {
FILE: src/workflowEngine/WorkflowEngine.js
class WorkflowEngine (line 15) | class WorkflowEngine {
method constructor (line 16) | constructor(workflow, { states, logger, blocksHandler, isPopup, option...
method init (line 123) | async init() {
method addRefDataSnapshot (line 348) | addRefDataSnapshot(key) {
method addWorker (line 356) | addWorker(detail) {
method addLogHistory (line 366) | addLogHistory(detail) {
method stop (line 402) | async stop() {
method executeQueue (line 414) | async executeQueue() {
method destroyWorker (line 434) | async destroyWorker(workerId) {
method destroy (line 453) | async destroy(status, message, blockDetail) {
method updateState (line 628) | async updateState(data) {
method dispatchEvent (line 653) | dispatchEvent(name, params) {
method on (line 663) | on(name, listener) {
FILE: src/workflowEngine/WorkflowLogger.js
class WorkflowLogger (line 3) | class WorkflowLogger {
method add (line 4) | async add({ detail, history, ctxData, data }) {
FILE: src/workflowEngine/WorkflowManager.js
method get (line 13) | get() {
method set (line 18) | set(key, value) {
class WorkflowManager (line 25) | class WorkflowManager {
method instance (line 33) | static get instance() {
method constructor (line 45) | constructor() {
method execute (line 50) | execute(workflowData, options) {
method stopExecution (line 154) | stopExecution(stateId) {
method resumeExecution (line 164) | resumeExecution(id, nextBlock) {
method updateExecution (line 174) | updateExecution(id, stateData) {
FILE: src/workflowEngine/WorkflowState.js
class WorkflowState (line 4) | class WorkflowState {
method constructor (line 5) | constructor({ storage, key = 'workflowState' }) {
method _updateBadge (line 15) | _updateBadge() {
method _saveToStorage (line 21) | _saveToStorage() {
method dispatchEvent (line 32) | dispatchEvent(name, params) {
method on (line 42) | on(name, listener) {
method off (line 48) | off(name, listener) {
method getAll (line 56) | get getAll() {
method get (line 60) | async get(stateId) {
method add (line 74) | async add(id, data = {}) {
method stop (line 80) | async stop(id) {
method resume (line 93) | async resume(id, nextBlock) {
method update (line 106) | async update(id, data = {}) {
method delete (line 120) | async delete(id) {
FILE: src/workflowEngine/WorkflowWorker.js
function blockExecutionWrapper (line 15) | function blockExecutionWrapper(blockHandler, blockData) {
class WorkflowWorker (line 38) | class WorkflowWorker {
method constructor (line 39) | constructor(id, engine, options = {}) {
method init (line 66) | init({ blockId, execParam, state }) {
method addDataToColumn (line 77) | addDataToColumn(key, value) {
method setVariable (line 112) | async setVariable(name, value) {
method getBlockConnections (line 143) | getBlockConnections(blockId, outputIndex = 1) {
method executeNextBlocks (line 154) | executeNextBlocks(
method resume (line 219) | resume(nextBlock) {
method executeBlock (line 232) | async executeBlock(block, execParam = {}, isRetry = false) {
method reset (line 459) | reset() {
method _sendMessageToTab (line 494) | async _sendMessageToTab(payload, options = {}, runBeforeLoad = false) {
FILE: src/workflowEngine/blocksHandler/handlerActiveTab.js
function activeTab (line 5) | async function activeTab(block) {
FILE: src/workflowEngine/blocksHandler/handlerAiWorkflow.js
function aiWorkflow (line 4) | async function aiWorkflow(block, { refData }) {
FILE: src/workflowEngine/blocksHandler/handlerBlocksGroup.js
function blocksGroup (line 1) | function blocksGroup({ data, id }, { prevBlockData }) {
FILE: src/workflowEngine/blocksHandler/handlerBrowserEvent.js
function handleEventListener (line 4) | function handleEventListener(target, validate) {
function onTabLoaded (line 27) | function onTabLoaded({ tabLoadedUrl, activeTabLoaded, timeout }, { id }) {
FILE: src/workflowEngine/blocksHandler/handlerClipboard.js
function doCommand (line 5) | function doCommand(command, value) {
FILE: src/workflowEngine/blocksHandler/handlerCloseTab.js
function closeWindow (line 3) | async function closeWindow(data, windowId) {
function closeTab (line 29) | async function closeTab(data, tabId) {
FILE: src/workflowEngine/blocksHandler/handlerConditions.js
function checkConditions (line 8) | function checkConditions(data, conditionOptions) {
function conditions (line 47) | async function conditions({ data, id }, { prevBlockData, refData }) {
FILE: src/workflowEngine/blocksHandler/handlerCookie.js
function getValues (line 4) | function getValues(data, keys) {
function cookie (line 35) | async function cookie({ data, id }) {
FILE: src/workflowEngine/blocksHandler/handlerCreateElement.js
function getAutomaScript (line 7) | function getAutomaScript(refData) {
function createElementScript (line 27) | function createElementScript(code, blockId, $automaScript, $preloadScrip...
function handleCreateElement (line 53) | async function handleCreateElement(block, { refData }) {
FILE: src/workflowEngine/blocksHandler/handlerDataMapping.js
function mapData (line 4) | function mapData(data, sources) {
function dataMapping (line 21) | async function dataMapping({ id, data }) {
FILE: src/workflowEngine/blocksHandler/handlerDelay.js
function delay (line 1) | function delay(block) {
FILE: src/workflowEngine/blocksHandler/handlerDeleteData.js
function deleteData (line 1) | function deleteData({ data, id }) {
FILE: src/workflowEngine/blocksHandler/handlerElementExists.js
function elementExists (line 1) | function elementExists(block) {
FILE: src/workflowEngine/blocksHandler/handlerExecuteWorkflow.js
function workflowListener (line 8) | function workflowListener(workflow, options) {
function findWorkflow (line 40) | function findWorkflow(workflows, workflowId) {
function executeWorkflow (line 48) | async function executeWorkflow({ id: blockId, data }, { refData }) {
FILE: src/workflowEngine/blocksHandler/handlerExportData.js
function blobToBase64 (line 4) | function blobToBase64(blob) {
function exportData (line 12) | async function exportData({ data, id }, { refData }) {
FILE: src/workflowEngine/blocksHandler/handlerForwardPage.js
function goBack (line 3) | async function goBack({ id }) {
FILE: src/workflowEngine/blocksHandler/handlerGoBack.js
function goBack (line 3) | async function goBack({ id }) {
FILE: src/workflowEngine/blocksHandler/handlerGoogleDrive.js
function getFilename (line 6) | function getFilename(url) {
function googleDrive (line 19) | async function googleDrive({ id, data }, { refData }) {
FILE: src/workflowEngine/blocksHandler/handlerGoogleSheets.js
function getSpreadsheetValues (line 9) | async function getSpreadsheetValues({
function getSpreadsheetRange (line 31) | async function getSpreadsheetRange({ spreadsheetId, range, isDriveSheet ...
function clearSpreadsheetValues (line 49) | async function clearSpreadsheetValues({ spreadsheetId, range, isDriveShe...
function updateSpreadsheetValues (line 62) | async function updateSpreadsheetValues(
FILE: src/workflowEngine/blocksHandler/handlerHandleDialog.js
function handleDialog (line 21) | async function handleDialog({ data, id: blockId }) {
FILE: src/workflowEngine/blocksHandler/handlerHandleDownload.js
constant DOWNLOADS_STORAGE_KEY (line 6) | const DOWNLOADS_STORAGE_KEY = 'automa-rename-downloaded-files';
function getDownloadFilesFromStorage (line 12) | async function getDownloadFilesFromStorage() {
function saveDownloadFilesToStorage (line 26) | async function saveDownloadFilesToStorage(filesData) {
function removeDownloadFromStorage (line 40) | async function removeDownloadFromStorage(downloadId) {
function registerDownloadListeners (line 53) | async function registerDownloadListeners() {
function handleDownload (line 80) | async function handleDownload({ data, id: blockId }) {
FILE: src/workflowEngine/blocksHandler/handlerHoverElement.js
function hoverElement (line 4) | async function hoverElement(block) {
FILE: src/workflowEngine/blocksHandler/handlerIncreaseVariable.js
function increaseVariable (line 3) | async function increaseVariable({ id, data }) {
FILE: src/workflowEngine/blocksHandler/handlerInsertData.js
function insertData (line 7) | async function insertData({ id, data }, { refData }) {
FILE: src/workflowEngine/blocksHandler/handlerInteractionBlock.js
function checkAccess (line 5) | async function checkAccess(blockName) {
function interactionHandler (line 26) | async function interactionHandler(block) {
FILE: src/workflowEngine/blocksHandler/handlerJavascriptCode.js
function getAutomaScript (line 15) | function getAutomaScript({ varName, refData, everyNewTab, isEval = false...
function executeInWebpage (line 59) | async function executeInWebpage(args, target, worker) {
function javascriptCode (line 260) | async function javascriptCode({ outputs, data, ...block }, { refData }) {
FILE: src/workflowEngine/blocksHandler/handlerLogData.js
function logData (line 3) | async function logData({ id, data }) {
FILE: src/workflowEngine/blocksHandler/handlerLoopBreakpoint.js
function loopBreakpoint (line 3) | async function loopBreakpoint(block, { prevBlockData }) {
FILE: src/workflowEngine/blocksHandler/handlerLoopData.js
function loopData (line 4) | async function loopData({ data, id }, { refData }) {
FILE: src/workflowEngine/blocksHandler/handlerLoopElements.js
function loopElements (line 1) | async function loopElements({ data, id }, { refData }) {
FILE: src/workflowEngine/blocksHandler/handlerNewTab.js
function isValidURL (line 11) | function isValidURL(url) {
function newTab (line 21) | async function newTab({ id, data }) {
FILE: src/workflowEngine/blocksHandler/handlerNewWindow.js
function newWindow (line 4) | async function newWindow({ data, id }) {
FILE: src/workflowEngine/blocksHandler/handlerParameterPrompt.js
function getInputtedParams (line 7) | function getInputtedParams(promptId, ms = 10000) {
function renderParamValue (line 38) | async function renderParamValue(param, refData, isPopup) {
FILE: src/workflowEngine/blocksHandler/handlerProxy.js
function setProxy (line 4) | function setProxy({ data, id }) {
FILE: src/workflowEngine/blocksHandler/handlerRegexVariable.js
function regexVariable (line 3) | async function regexVariable({ id, data }) {
FILE: src/workflowEngine/blocksHandler/handlerReloadTab.js
function reloadTab (line 3) | async function reloadTab({ id }) {
FILE: src/workflowEngine/blocksHandler/handlerRepeatTask.js
function repeatTask (line 1) | function repeatTask({ data, id }) {
FILE: src/workflowEngine/blocksHandler/handlerSaveAssets.js
function getFilename (line 3) | function getFilename(url) {
FILE: src/workflowEngine/blocksHandler/handlerSliceVariable.js
function sliceData (line 3) | async function sliceData({ id, data }) {
FILE: src/workflowEngine/blocksHandler/handlerSortData.js
function sliceData (line 3) | async function sliceData({ id, data }) {
FILE: src/workflowEngine/blocksHandler/handlerSwitchTo.js
function switchTo (line 4) | async function switchTo(block) {
FILE: src/workflowEngine/blocksHandler/handlerTabUrl.js
function logData (line 3) | async function logData({ id, data }) {
FILE: src/workflowEngine/blocksHandler/handlerTakeScreenshot.js
function saveImage (line 6) | async function saveImage({ filename, uri, ext }) {
function takeScreenshot (line 37) | async function takeScreenshot({ data, id, label }) {
FILE: src/workflowEngine/blocksHandler/handlerTrigger.js
function trigger (line 1) | async function trigger(block) {
FILE: src/workflowEngine/blocksHandler/handlerWaitConnections.js
function waitConnections (line 1) | async function waitConnections({ data, id }, { prevBlock }) {
FILE: src/workflowEngine/blocksHandler/handlerWebhook.js
constant ALL_HTTP_RESPONSE_KEYWORD (line 6) | const ALL_HTTP_RESPONSE_KEYWORD = '$response';
function webhook (line 8) | async function webhook({ data, id }, { refData }) {
FILE: src/workflowEngine/blocksHandler/handlerWhileLoop.js
function whileLoop (line 4) | async function whileLoop({ data, id }, { refData }) {
FILE: src/workflowEngine/helper.js
function escapeElementPolicy (line 6) | function escapeElementPolicy(script) {
function messageSandbox (line 47) | function messageSandbox(type, data = {}) {
function getFrames (line 69) | async function getFrames(tabId) {
function sendDebugCommand (line 89) | function sendDebugCommand(tabId, method, params = {}) {
function attachDebugger (line 95) | async function attachDebugger(tabId, prevTab) {
function waitTabLoaded (line 114) | function waitTabLoaded({ tabId, listenError = false, ms = 10000 }) {
function convertData (line 174) | function convertData(data, type) {
function automaRefDataStr (line 199) | function automaRefDataStr(varName) {
function injectPreloadScript (line 234) | function injectPreloadScript({ target, scripts, frameSelector }) {
function checkCSPAndInject (line 269) | async function checkCSPAndInject(
function fallbackCopyTextToClipboard (line 300) | function fallbackCopyTextToClipboard(text) {
function copyTextToClipboard (line 322) | function copyTextToClipboard(text) {
FILE: src/workflowEngine/injectContentScript.js
function contentScriptExist (line 5) | async function contentScriptExist(tabId, frameId = 0) {
FILE: src/workflowEngine/templating/mustacheReplacer.js
function extractStrFunction (line 12) | function extractStrFunction(str) {
function keyParser (line 29) | function keyParser(key, data) {
function replacer (line 64) | function replacer(
FILE: src/workflowEngine/templating/templatingFunctions.js
function parseJSON (line 12) | function parseJSON(data, def) {
method date (line 23) | date(...args) {
method randint (line 45) | randint(min = 0, max = 100) {
method getLength (line 48) | getLength(str) {
method slice (line 53) | slice(value, start, end) {
method multiply (line 61) | multiply(value, multiplyBy) {
method increment (line 66) | increment(value, incrementBy) {
method divide (line 71) | divide(value, divideBy) {
method subtract (line 76) | subtract(value, subtractBy) {
method randData (line 81) | randData(str) {
method filter (line 119) | filter(data, exps) {
method replace (line 124) | replace(value, search, replace) {
method replaceAll (line 129) | replaceAll(value, search, replace) {
method toLowerCase (line 134) | toLowerCase(value) {
method toUpperCase (line 139) | toUpperCase(value) {
method modulo (line 144) | modulo(value, divisor) {
method stringify (line 147) | stringify(value) {
FILE: src/workflowEngine/utils/javascriptBlockUtil.js
function automaFetchClient (line 3) | function automaFetchClient(id, { type, resource }) {
function jsContentHandlerEval (line 37) | function jsContentHandlerEval({
function jsContentHandler (line 74) | function jsContentHandler($blockData, $preloadScripts, $automaScript) {
FILE: src/workflowEngine/utils/testConditions.js
function getConditionItemValue (line 51) | async function getConditionItemValue({ type, data }) {
function checkConditions (line 143) | async function checkConditions(items) {
FILE: src/workflowEngine/utils/webhookUtil.js
function executeWebhook (line 84) | async function executeWebhook({
FILE: src/workflowEngine/workflowEvent.js
class WorkflowEvent (line 5) | class WorkflowEvent {
method #httpRequest (line 6) | static async #httpRequest({ url, method, headers, body }, refData) {
method #javascriptCode (line 26) | static async #javascriptCode(event, refData) {
method handle (line 39) | static async handle(event, refData) {
FILE: tailwind.config.js
function withOpacityValue (line 5) | function withOpacityValue(variable) {
function rem2px (line 13) | function rem2px(input, fontSize = 16) {
FILE: webpack.config.js
constant ASSET_PATH (line 12) | const ASSET_PATH = process.env.ASSET_PATH || '/';
method transform (line 167) | transform(content) {
Condensed preview — 515 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (2,296K chars).
[
{
"path": ".babelrc",
"chars": 196,
"preview": "{\n \"plugins\": [],\n \"presets\": [\n [\"@babel/preset-env\", {\n \"useBuiltIns\": \"usage\",\n \"corejs\": 3,\n \"ta"
},
{
"path": ".editorconfig",
"chars": 335,
"preview": "# EditorConfig is awesome: http://EditorConfig.org\n\n# https://github.com/jokeyrhyme/standard-editorconfig\n\n# top-most Ed"
},
{
"path": ".eslintrc.js",
"chars": 1963,
"preview": "// https://eslint.org/docs/user-guide/configuring\n// File taken from https://github.com/vuejs-templates/webpack/blob/1.3"
},
{
"path": ".github/FUNDING.yml",
"chars": 66,
"preview": "# These are supported funding model platforms\n\ngithub: AutomaApp\n\n"
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.md",
"chars": 685,
"preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Describe the b"
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.md",
"chars": 595,
"preview": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Is your fea"
},
{
"path": ".github/dependabot.yml",
"chars": 138,
"preview": "version: 2\nupdates:\n - package-ecosystem: \"npm\"\n directory: \"/\"\n target-branch: \"dev\"\n schedule:\n interva"
},
{
"path": ".gitignore",
"chars": 405,
"preview": "# See https://help.github.com/ignore-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n\n# testing\n/cov"
},
{
"path": ".prettierrc",
"chars": 79,
"preview": "{\n \"singleQuote\": true,\n \"trailingComma\": \"es5\",\n \"arrowParens\": \"always\"\n}\n"
},
{
"path": ".vscode/settings.json",
"chars": 90,
"preview": "{\n \"i18n-ally.localesPaths\": [\n \"src/locales\"\n ],\n \"i18n-ally.keystyle\": \"nested\"\n}\n"
},
{
"path": "LICENSE.txt",
"chars": 723,
"preview": "Source code in this repository is variously licensed under the GNU Affero General Public License (AGPL), or the Automa C"
},
{
"path": "README.md",
"chars": 3939,
"preview": "<img src=\"src/assets/images/icon-128.png\" width=\"64\"/>\n\n# Automa\n<p>\n <img alt=\"Automa latest version\" src=\"https://img"
},
{
"path": "business/dev/blocks/backgroundHandler/index.js",
"chars": 44,
"preview": "export default function () {\n return {};\n}\n"
},
{
"path": "business/dev/blocks/contentHandler/index.js",
"chars": 44,
"preview": "export default function () {\n return {};\n}\n"
},
{
"path": "business/dev/blocks/editComponents/index.js",
"chars": 44,
"preview": "export default function () {\n return {};\n}\n"
},
{
"path": "business/dev/blocks/index.js",
"chars": 44,
"preview": "export default function () {\n return {};\n}\n"
},
{
"path": "business/dev/index.js",
"chars": 30,
"preview": "export default function () {}\n"
},
{
"path": "business/dev/parameters/index.js",
"chars": 44,
"preview": "export default function () {\n return {};\n}\n"
},
{
"path": "jsconfig.json",
"chars": 254,
"preview": "{\n \"compilerOptions\": {\n \"baseUrl\": \"./\",\n \"paths\": {\n \"@/*\": [\"src/*\"],\n \"@business\": [\"business/dev/*"
},
{
"path": "package.json",
"chars": 4920,
"preview": "{\n \"name\": \"automa\",\n \"version\": \"1.30.00\",\n \"description\": \"An extension for automating your browser by connecting b"
},
{
"path": "postcss.config.js",
"chars": 114,
"preview": "module.exports = {\n plugins: {\n 'tailwindcss/nesting': {},\n tailwindcss: {},\n autoprefixer: {},\n },\n};\n"
},
{
"path": "secrets.blank.js",
"chars": 38,
"preview": "export default {\n baseApiUrl: '',\n};\n"
},
{
"path": "src/assets/css/drawflow.css",
"chars": 2775,
"preview": ".drawflow-node.selected-list .menu,\n.drawflow-node.selected .menu,\n.drawflow-node .block-base:hover .menu {\n @apply tra"
},
{
"path": "src/assets/css/flow.css",
"chars": 1025,
"preview": ".vue-flow__minimap {\n\t@apply rounded-lg dark:bg-gray-800;\n}\n\n.vue-flow__node {\n\t& > div {\n\t\t@apply rounded-lg transition"
},
{
"path": "src/assets/css/fonts.css",
"chars": 991,
"preview": "@font-face {\n font-family: Inter var;\n font-weight: 100 900;\n font-display: swap;\n font-style: normal;\n font-named-"
},
{
"path": "src/assets/css/style.css",
"chars": 239,
"preview": ".list-item-transition {\n transition: all 0.4s ease;\n}\n\n.list-leave-active {\n position: absolute;\n width: 100%;\n}\n\n.li"
},
{
"path": "src/assets/css/tailwind.css",
"chars": 2549,
"preview": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n@layer utilities {\n .hoverable {\n @apply hover:bg-gray-8"
},
{
"path": "src/background/BackgroundEventsListeners.js",
"chars": 4609,
"preview": "import browser from 'webextension-polyfill';\nimport { initElementSelector } from '@/newtab/utils/elementSelector';\nimpor"
},
{
"path": "src/background/BackgroundOffscreen.js",
"chars": 1839,
"preview": "/* eslint-disable class-methods-use-this */\nimport { IS_FIREFOX } from '@/common/utils/constant';\nimport { sleep } from "
},
{
"path": "src/background/BackgroundUtils.js",
"chars": 1591,
"preview": "import browser from 'webextension-polyfill';\nimport { waitTabLoaded } from '@/workflowEngine/helper';\n\nclass BackgroundU"
},
{
"path": "src/background/BackgroundWorkflowTriggers.js",
"chars": 6179,
"preview": "import browser from 'webextension-polyfill';\nimport dayjs from 'dayjs';\nimport { findTriggerBlock, parseJSON } from '@/u"
},
{
"path": "src/background/BackgroundWorkflowUtils.js",
"chars": 3466,
"preview": "import { IS_FIREFOX } from '@/common/utils/constant';\nimport browser from 'webextension-polyfill';\nimport BackgroundOffs"
},
{
"path": "src/background/index.js",
"chars": 32310,
"preview": "import { IS_FIREFOX } from '@/common/utils/constant';\nimport BrowserAPIEventHandler from '@/service/browser-api/BrowserA"
},
{
"path": "src/common/utils/constant.js",
"chars": 54,
"preview": "export const IS_FIREFOX = BROWSER_TYPE === 'firefox';\n"
},
{
"path": "src/components/block/BlockBase.vue",
"chars": 4093,
"preview": "<template>\n <div\n class=\"block-base relative w-48\"\n :data-block-id=\"blockId\"\n @dblclick.stop=\"$emit('edit')\"\n "
},
{
"path": "src/components/block/BlockBasic.vue",
"chars": 5505,
"preview": "<template>\n <block-base\n :id=\"componentId\"\n :data=\"data\"\n :block-id=\"id\"\n :block-data=\"block\"\n :data-pos"
},
{
"path": "src/components/block/BlockBasicWithFallback.vue",
"chars": 2685,
"preview": "<template>\n <block-base\n :id=\"componentId\"\n :data=\"data\"\n :block-id=\"id\"\n :block-data=\"block\"\n class=\"bl"
},
{
"path": "src/components/block/BlockConditions.vue",
"chars": 3136,
"preview": "<template>\n <block-base\n :id=\"componentId\"\n :data=\"data\"\n :block-id=\"id\"\n :block-data=\"block\"\n class=\"w-"
},
{
"path": "src/components/block/BlockDelay.vue",
"chars": 2532,
"preview": "<template>\n <block-base\n :id=\"componentId\"\n :data=\"data\"\n :block-id=\"id\"\n :block-data=\"block\"\n class=\"w-"
},
{
"path": "src/components/block/BlockElementExists.vue",
"chars": 2303,
"preview": "<template>\n <block-base\n :id=\"componentId\"\n :data=\"data\"\n :block-id=\"id\"\n :block-data=\"block\"\n style=\"wi"
},
{
"path": "src/components/block/BlockGroup.vue",
"chars": 7265,
"preview": "<template>\n <block-base\n :id=\"componentId\"\n :data=\"data\"\n :block-id=\"id\"\n :block-data=\"block\"\n class=\"w-"
},
{
"path": "src/components/block/BlockGroup2.vue",
"chars": 2728,
"preview": "<template>\n <div\n :style=\"{\n width: `${data.width || 400}px`,\n height: `${data.height || 300}px`,\n }\"\n "
},
{
"path": "src/components/block/BlockLoopBreakpoint.vue",
"chars": 2120,
"preview": "<template>\n <block-base\n :id=\"componentId\"\n :data=\"data\"\n :block-id=\"id\"\n :block-data=\"block\"\n class=\"w-"
},
{
"path": "src/components/block/BlockNote.vue",
"chars": 3594,
"preview": "<template>\n <div\n :class=\"[data.color || 'white', colors[data.color || 'white']]\"\n class=\"block-note rounded-lg p"
},
{
"path": "src/components/block/BlockPackage.vue",
"chars": 4964,
"preview": "<template>\n <block-base\n :id=\"componentId\"\n :data=\"data\"\n :block-id=\"id\"\n :block-data=\"block\"\n class=\"bl"
},
{
"path": "src/components/block/BlockRepeatTask.vue",
"chars": 2422,
"preview": "<template>\n <block-base\n :id=\"componentId\"\n :data=\"data\"\n :block-id=\"id\"\n :block-data=\"block\"\n class=\"re"
},
{
"path": "src/components/content/selector/SelectorBlocks.vue",
"chars": 3080,
"preview": "<template>\n <div class=\"events mt-4\">\n <div class=\"flex items-center\">\n <ui-select\n v-model=\"state.selec"
},
{
"path": "src/components/content/selector/SelectorElementList.vue",
"chars": 603,
"preview": "<template>\n <ul class=\"mt-2 space-y-4\">\n <li\n v-for=\"(element, index) in elements\"\n :key=\"index\"\n @mo"
},
{
"path": "src/components/content/selector/SelectorElementsDetail.vue",
"chars": 4120,
"preview": "<template>\n <ui-tabs\n v-if=\"!hideBlocks || selectElements.length > 0\"\n :model-value=\"activeTab\"\n class=\"mt-2\"\n"
},
{
"path": "src/components/content/selector/SelectorQuery.vue",
"chars": 2792,
"preview": "<template>\n <div>\n <div class=\"flex items-center\">\n <ui-select\n :model-value=\"selectorType\"\n :dis"
},
{
"path": "src/components/content/shared/SharedElementHighlighter.vue",
"chars": 1122,
"preview": "<template>\n <rect\n v-for=\"(item, index) in items\"\n v-bind=\"{\n x: getNumber(item?.x),\n y: getNumber(item"
},
{
"path": "src/components/content/shared/SharedElementSelector.vue",
"chars": 8955,
"preview": "<template>\n <svg\n v-if=\"!disabled\"\n class=\"automa-element-highlighter\"\n style=\"\n height: 100%;\n widt"
},
{
"path": "src/components/newtab/app/AppLogs.vue",
"chars": 1380,
"preview": "<template>\n <ui-modal\n v-model=\"state.show\"\n custom-content\n content-position=\"start\"\n @close=\"clearState\"\n"
},
{
"path": "src/components/newtab/app/AppLogsItem.vue",
"chars": 5329,
"preview": "<template>\n <div v-if=\"currentLog.id\">\n <div class=\"flex items-center\">\n <button\n v-tooltip:bottom=\"t('w"
},
{
"path": "src/components/newtab/app/AppLogsItemRunning.vue",
"chars": 3560,
"preview": "<template>\n <div v-if=\"running\">\n <div class=\"flex items-center\">\n <button\n v-tooltip:bottom=\"t('workflo"
},
{
"path": "src/components/newtab/app/AppLogsItems.vue",
"chars": 9456,
"preview": "<template>\n <div class=\"logs-list overflow-auto pb-4 pt-1\">\n <div class=\"mb-8 flex items-center\">\n <h1 class=\"f"
},
{
"path": "src/components/newtab/app/AppSidebar.vue",
"chars": 6004,
"preview": "<template>\n <aside\n class=\"fixed left-0 top-0 z-50 flex h-screen w-16 flex-col items-center bg-white py-6 dark:bg-gr"
},
{
"path": "src/components/newtab/app/AppSurvey.vue",
"chars": 2589,
"preview": "<template>\n <ui-card\n v-if=\"modalState.show\"\n class=\"group fixed bottom-8 right-8 w-72 border-2 shadow-2xl\"\n >\n "
},
{
"path": "src/components/newtab/logs/LogsDataViewer.vue",
"chars": 2855,
"preview": "<template>\n <div v-if=\"state.status === 'loading'\" class=\"py-8 text-center\">\n <ui-spinner color=\"text-primary\" />\n "
},
{
"path": "src/components/newtab/logs/LogsFilters.vue",
"chars": 3629,
"preview": "<template>\n <div class=\"mb-6 flex flex-wrap items-center md:space-x-4\">\n <ui-input\n id=\"search-input\"\n :mo"
},
{
"path": "src/components/newtab/logs/LogsHistory.vue",
"chars": 17507,
"preview": "<template>\n <router-link\n v-if=\"parentLog\"\n replace\n :to=\"'/logs/' + currentLog.parentLog?.id || currentLog.co"
},
{
"path": "src/components/newtab/logs/LogsTable.vue",
"chars": 2616,
"preview": "<template>\n <div v-if=\"tableData.body.length === 0\" class=\"text-center\">\n <img src=\"@/assets/svg/files-and-folder.sv"
},
{
"path": "src/components/newtab/logs/LogsVariables.vue",
"chars": 1935,
"preview": "<template>\n <div v-if=\"Object.keys(variables).length === 0\" class=\"text-center\">\n <img src=\"@/assets/svg/files-and-f"
},
{
"path": "src/components/newtab/package/PackageDetails.vue",
"chars": 1557,
"preview": "<template>\n <div class=\"w-full max-w-2xl pb-8\">\n <ui-input\n :model-value=\"data.name\"\n label=\"Package name\""
},
{
"path": "src/components/newtab/package/PackageSettingIOSelect.vue",
"chars": 3661,
"preview": "<template>\n <ui-popover>\n <template #trigger>\n <ui-button class=\"w-full\">\n Select block {{ props.data.bl"
},
{
"path": "src/components/newtab/package/PackageSettings.vue",
"chars": 6423,
"preview": "<template>\n <label class=\"inline-flex items-center\">\n <ui-switch v-model=\"packageState.settings.asBlock\" />\n <spa"
},
{
"path": "src/components/newtab/settings/SettingsBackupItems.vue",
"chars": 2955,
"preview": "<template>\n <div class=\"scroll content w-full overflow-auto\">\n <div v-if=\"!query && workflows.length === 0\" class=\"t"
},
{
"path": "src/components/newtab/settings/SettingsCloudBackup.vue",
"chars": 13478,
"preview": "<template>\n <div class=\"cloud-backup mt-4 flex items-start\">\n <div class=\"w-56\">\n <ui-input\n v-model=\"st"
},
{
"path": "src/components/newtab/settings/jsBlockWrap.js",
"chars": 122,
"preview": "import { reactive } from 'vue';\n\nexport const store = reactive({\n whiteSpace: 'pre',\n statePrettier: Math.random(),\n})"
},
{
"path": "src/components/newtab/shared/SharedCard.vue",
"chars": 2808,
"preview": "<template>\n <ui-card\n :data-workflow-id=\"data.hostId\"\n class=\"group flex flex-col hover:ring-2 hover:ring-accent "
},
{
"path": "src/components/newtab/shared/SharedCodemirror.vue",
"chars": 3184,
"preview": "<template>\n <div\n ref=\"containerEl\"\n :class=\"{ 'hide-gutters': !lineNumbers }\"\n class=\"codemirror relative rou"
},
{
"path": "src/components/newtab/shared/SharedConditionBuilder/ConditionBuilderInputs.vue",
"chars": 6259,
"preview": "<template>\n <div\n v-for=\"(item, index) in inputsData\"\n :key=\"item.id\"\n class=\"condition-input scroll\"\n >\n "
},
{
"path": "src/components/newtab/shared/SharedConditionBuilder/index.vue",
"chars": 6808,
"preview": "<template>\n <div class=\"space-y-4\">\n <ui-button v-if=\"conditions.length === 0\" @click=\"addOrCondition\">\n {{ t('"
},
{
"path": "src/components/newtab/shared/SharedElSelectorActions.vue",
"chars": 1393,
"preview": "<template>\n <div class=\"inline-flex items-center\">\n <ui-button\n v-tooltip.group=\"$t('workflow.blocks.base.eleme"
},
{
"path": "src/components/newtab/shared/SharedLogsTable.vue",
"chars": 6256,
"preview": "<template>\n <div class=\"logs-table scroll overflow-x-auto\">\n <transition-expand>\n <div v-if=\"state.selected.len"
},
{
"path": "src/components/newtab/shared/SharedPermissionsModal.vue",
"chars": 1832,
"preview": "<template>\n <ui-modal :title=\"t('workflowPermissions.title')\" persist>\n <p class=\"font-semibold\">\n {{ t('workfl"
},
{
"path": "src/components/newtab/shared/SharedWorkflowState.vue",
"chars": 2494,
"preview": "<template>\n <ui-card>\n <div class=\"mb-4 flex items-center\">\n <div class=\"text-overflow mr-4 flex-1\">\n <p"
},
{
"path": "src/components/newtab/shared/SharedWorkflowTriggers.vue",
"chars": 4282,
"preview": "<template>\n <div\n class=\"scroll overflow-auto\"\n style=\"min-height: 350px; max-height: calc(100vh - 14rem)\"\n >\n "
},
{
"path": "src/components/newtab/shared/SharedWysiwyg.vue",
"chars": 5602,
"preview": "<template>\n <div class=\"wysiwyg-editor\">\n <slot v-if=\"editor\" name=\"prepend\" :editor=\"editor\" />\n <div\n v-if"
},
{
"path": "src/components/newtab/storage/StorageCredentials.vue",
"chars": 3428,
"preview": "<template>\n <div class=\"mt-6 flex\">\n <ui-input\n v-model=\"state.query\"\n :placeholder=\"t('common.search')\"\n "
},
{
"path": "src/components/newtab/storage/StorageEditTable.vue",
"chars": 4868,
"preview": "<template>\n <ui-modal :model-value=\"modelValue\" persist custom-content>\n <ui-card\n padding=\"p-0\"\n class=\"f"
},
{
"path": "src/components/newtab/storage/StorageTables.vue",
"chars": 3971,
"preview": "<template>\n <div class=\"mt-6 flex\">\n <ui-input\n v-model=\"state.query\"\n :placeholder=\"t('common.search')\"\n "
},
{
"path": "src/components/newtab/storage/StorageVariables.vue",
"chars": 3791,
"preview": "<template>\n <div class=\"mt-6 flex\">\n <ui-input\n v-model=\"state.query\"\n :placeholder=\"t('common.search')\"\n "
},
{
"path": "src/components/newtab/workflow/WorkflowBlockList.vue",
"chars": 3402,
"preview": "<template>\n <ui-expand\n hide-header-icon\n header-class=\"flex items-center py-2 focus:ring-0 w-full text-left text"
},
{
"path": "src/components/newtab/workflow/WorkflowDataTable.vue",
"chars": 5918,
"preview": "<template>\n <template v-if=\"!workflow.connectedTable\">\n <ui-popover class=\"mb-4\">\n <template #trigger>\n "
},
{
"path": "src/components/newtab/workflow/WorkflowDetailsCard.vue",
"chars": 5141,
"preview": "<template>\n <div class=\"mb-2 mt-1 flex items-start px-4\">\n <ui-popover class=\"mr-2 h-8\">\n <template #trigger>\n "
},
{
"path": "src/components/newtab/workflow/WorkflowEditBlock.vue",
"chars": 3685,
"preview": "<template>\n <div id=\"workflow-edit-block\" class=\"scroll h-full overflow-auto px-4 py-1\">\n <div\n class=\"sticky t"
},
{
"path": "src/components/newtab/workflow/WorkflowEditor.vue",
"chars": 8788,
"preview": "<template>\n <vue-flow\n :id=\"props.id\"\n :class=\"{ disabled: isDisabled }\"\n :default-edge-options=\"{\n type:"
},
{
"path": "src/components/newtab/workflow/WorkflowGlobalData.vue",
"chars": 1008,
"preview": "<template>\n <div class=\"global-data\">\n <p class=\"text-right\" title=\"Characters limit\">\n {{ globalData.length }}"
},
{
"path": "src/components/newtab/workflow/WorkflowProtect.vue",
"chars": 1807,
"preview": "<template>\n <div>\n <form\n class=\"mb-4 flex w-full items-center\"\n @submit.prevent=\"protectWorkflow\"\n >\n "
},
{
"path": "src/components/newtab/workflow/WorkflowRunning.vue",
"chars": 2313,
"preview": "<template>\n <div class=\"grid grid-cols-2 gap-4\">\n <ui-card v-for=\"item in data\" :key=\"item\">\n <div class=\"mb-4 "
},
{
"path": "src/components/newtab/workflow/WorkflowSettings.vue",
"chars": 2642,
"preview": "<template>\n <ui-card padding=\"p-0\" class=\"workflow-settings w-full max-w-2xl\">\n <div class=\"flex items-center px-4 p"
},
{
"path": "src/components/newtab/workflow/WorkflowShare.vue",
"chars": 6033,
"preview": "<template>\n <ui-card class=\"share-workflow scroll w-full max-w-2xl overflow-auto\">\n <template v-if=\"!userStore.user?"
},
{
"path": "src/components/newtab/workflow/WorkflowShareTeam.vue",
"chars": 8382,
"preview": "<template>\n <ui-card class=\"share-workflow scroll w-full max-w-4xl overflow-auto\">\n <template v-if=\"!isUpdate\">\n "
},
{
"path": "src/components/newtab/workflow/WorkflowSharedActions.vue",
"chars": 3781,
"preview": "<template>\n <ui-card padding=\"p-1\">\n <ui-input\n v-tooltip=\"t('workflow.share.url')\"\n prepend-icon=\"riLinkM"
},
{
"path": "src/components/newtab/workflow/edit/BlockSetting/BlockSettingGeneral.vue",
"chars": 1915,
"preview": "<template>\n <div class=\"block-setting-general\">\n <ui-list>\n <div v-if=\"props.data.id !== 'delay'\" class=\"flex i"
},
{
"path": "src/components/newtab/workflow/edit/BlockSetting/BlockSettingLines.vue",
"chars": 2835,
"preview": "<template>\n <div class=\"block-lines max-w-xl\">\n <ui-select\n v-model=\"state.activeEdge\"\n :placeholder=\"t('w"
},
{
"path": "src/components/newtab/workflow/edit/BlockSetting/BlockSettingOnError.vue",
"chars": 6370,
"preview": "<template>\n <div\n class=\"on-block-error scroll overflow-auto\"\n style=\"max-height: calc(100vh - 13rem)\"\n >\n <d"
},
{
"path": "src/components/newtab/workflow/edit/EditAiWorkflow.vue",
"chars": 9382,
"preview": "<template>\n <div>\n <ui-button\n variant=\"accent\"\n class=\"text-sm w-full\"\n @click=\"state.showAIPowerTok"
},
{
"path": "src/components/newtab/workflow/edit/EditAttributeValue.vue",
"chars": 1772,
"preview": "<template>\n <edit-interaction-base v-bind=\"{ data }\" @change=\"updateData\">\n <hr />\n <ui-select\n :label=\"t('c"
},
{
"path": "src/components/newtab/workflow/edit/EditAutocomplete.vue",
"chars": 1199,
"preview": "<template>\n <ui-autocomplete\n :items=\"autocompleteList\"\n :trigger-char=\"['{{', '}}']\"\n :custom-filter=\"autocom"
},
{
"path": "src/components/newtab/workflow/edit/EditBlockSettings.vue",
"chars": 2932,
"preview": "<template>\n <ui-tabs v-model=\"state.activeTab\" class=\"-mt-2\">\n <ui-tab v-for=\"tab in tabs\" :key=\"tab.id\" :value=\"tab"
},
{
"path": "src/components/newtab/workflow/edit/EditBrowserEvent.vue",
"chars": 3075,
"preview": "<template>\n <div>\n <ui-textarea\n :model-value=\"data.description\"\n class=\"w-full\"\n :placeholder=\"t('co"
},
{
"path": "src/components/newtab/workflow/edit/EditClipboard.vue",
"chars": 2374,
"preview": "<template>\n <div>\n <ui-textarea\n :model-value=\"data.description\"\n class=\"w-full\"\n :placeholder=\"t('co"
},
{
"path": "src/components/newtab/workflow/edit/EditCloseTab.vue",
"chars": 2285,
"preview": "<template>\n <div class=\"mb-2 mt-4\">\n <ui-textarea\n :model-value=\"data.description\"\n :placeholder=\"t('commo"
},
{
"path": "src/components/newtab/workflow/edit/EditConditions.vue",
"chars": 6653,
"preview": "<template>\n <div>\n <ui-textarea\n :model-value=\"data.description\"\n class=\"w-full\"\n :placeholder=\"t('co"
},
{
"path": "src/components/newtab/workflow/edit/EditCookie.vue",
"chars": 6905,
"preview": "<template>\n <div>\n <ui-textarea\n :model-value=\"data.description\"\n class=\"w-full\"\n :placeholder=\"t('co"
},
{
"path": "src/components/newtab/workflow/edit/EditCreateElement.vue",
"chars": 5899,
"preview": "<template>\n <edit-interaction-base\n :data=\"blockData\"\n hide-mark-el\n hide-multiple\n @change=\"updateSelector"
},
{
"path": "src/components/newtab/workflow/edit/EditDataMapping.vue",
"chars": 7031,
"preview": "<template>\n <div>\n <ui-textarea\n :model-value=\"data.description\"\n :placeholder=\"t('common.description')\"\n "
},
{
"path": "src/components/newtab/workflow/edit/EditDelay.vue",
"chars": 475,
"preview": "<template>\n <div class=\"space-y-2\">\n <ui-input\n :model-value=\"data.time\"\n label=\"Delay time (millisecond)\""
},
{
"path": "src/components/newtab/workflow/edit/EditDeleteData.vue",
"chars": 2805,
"preview": "<template>\n <div>\n <ui-textarea\n :model-value=\"data.description\"\n class=\"w-full\"\n :placeholder=\"t('co"
},
{
"path": "src/components/newtab/workflow/edit/EditElementExists.vue",
"chars": 2365,
"preview": "<template>\n <div>\n <ui-textarea\n :model-value=\"data.description\"\n :placeholder=\"t('common.description')\"\n "
},
{
"path": "src/components/newtab/workflow/edit/EditExecuteWorkflow.vue",
"chars": 4525,
"preview": "<template>\n <div class=\"mb-12\">\n <ui-textarea\n :model-value=\"data.description\"\n :placeholder=\"t('common.de"
},
{
"path": "src/components/newtab/workflow/edit/EditExportData.vue",
"chars": 4245,
"preview": "<template>\n <div>\n <ui-textarea\n :model-value=\"data.description\"\n class=\"w-full\"\n :placeholder=\"t('co"
},
{
"path": "src/components/newtab/workflow/edit/EditForms.vue",
"chars": 4114,
"preview": "<template>\n <edit-interaction-base v-bind=\"{ data, hide: hideBase }\" @change=\"updateData\">\n <hr />\n <ui-checkbox\n"
},
{
"path": "src/components/newtab/workflow/edit/EditGetText.vue",
"chars": 3371,
"preview": "<template>\n <edit-interaction-base v-bind=\"{ data }\" @change=\"updateData\">\n <hr />\n <div class=\"bg-input flex ite"
},
{
"path": "src/components/newtab/workflow/edit/EditGoogleDrive.vue",
"chars": 3829,
"preview": "<template>\n <div>\n <div v-if=\"!store.integrations.googleDrive\">\n <p>\n You haven't\n <a\n h"
},
{
"path": "src/components/newtab/workflow/edit/EditGoogleSheets.vue",
"chars": 11850,
"preview": "<template>\n <div class=\"mb-10\">\n <ui-textarea\n :model-value=\"data.description\"\n class=\"mb-2 w-full\"\n "
},
{
"path": "src/components/newtab/workflow/edit/EditGoogleSheetsDrive.vue",
"chars": 4216,
"preview": "<template>\n <div v-if=\"!store.integrations.googleDrive\">\n <p>\n You haven't\n <a\n href=\"https://docs."
},
{
"path": "src/components/newtab/workflow/edit/EditHandleDialog.vue",
"chars": 1206,
"preview": "<template>\n <div>\n <ui-textarea\n :model-value=\"data.description\"\n class=\"w-full\"\n :placeholder=\"t('co"
},
{
"path": "src/components/newtab/workflow/edit/EditHandleDownload.vue",
"chars": 2887,
"preview": "<template>\n <div>\n <template v-if=\"permission.has.downloads\">\n <ui-textarea\n :model-value=\"data.descript"
},
{
"path": "src/components/newtab/workflow/edit/EditIncreaseVariable.vue",
"chars": 1059,
"preview": "<template>\n <div>\n <ui-textarea\n :model-value=\"data.description\"\n :placeholder=\"t('common.description')\"\n "
},
{
"path": "src/components/newtab/workflow/edit/EditInsertData.vue",
"chars": 9545,
"preview": "<template>\n <div>\n <ui-textarea\n :model-value=\"data.description\"\n :placeholder=\"t('common.description')\"\n "
},
{
"path": "src/components/newtab/workflow/edit/EditInteractionBase.vue",
"chars": 4192,
"preview": "<template>\n <div>\n <slot name=\"prepend\" />\n <template v-if=\"!hide\">\n <ui-textarea\n v-if=\"!hideDescrip"
},
{
"path": "src/components/newtab/workflow/edit/EditJavascriptCode.vue",
"chars": 7265,
"preview": "<template>\n <div class=\"mb-2 mt-4\">\n <ui-textarea\n :model-value=\"data.description\"\n autoresize\n :plac"
},
{
"path": "src/components/newtab/workflow/edit/EditLink.vue",
"chars": 685,
"preview": "<template>\n <edit-interaction-base v-bind=\"{ data }\" @change=\"updateData\">\n <ui-checkbox\n :model-value=\"data.op"
},
{
"path": "src/components/newtab/workflow/edit/EditLogData.vue",
"chars": 1487,
"preview": "<template>\n <div>\n <ui-textarea\n :model-value=\"data.description\"\n :placeholder=\"t('common.description')\"\n "
},
{
"path": "src/components/newtab/workflow/edit/EditLoopData.vue",
"chars": 8606,
"preview": "<template>\n <div>\n <ui-textarea\n :model-value=\"data.description\"\n :placeholder=\"t('common.description')\"\n "
},
{
"path": "src/components/newtab/workflow/edit/EditLoopElements.vue",
"chars": 4152,
"preview": "<template>\n <edit-interaction-base\n :data=\"data\"\n hide-multiple\n hide-mark-el\n @change=\"updateData\"\n >\n "
},
{
"path": "src/components/newtab/workflow/edit/EditNewTab.vue",
"chars": 5142,
"preview": "<template>\n <div>\n <ui-textarea\n :model-value=\"data.description\"\n :placeholder=\"t('common.description')\"\n "
},
{
"path": "src/components/newtab/workflow/edit/EditNewWindow.vue",
"chars": 3298,
"preview": "<template>\n <div class=\"mb-2 mt-4\">\n <ui-textarea\n :model-value=\"data.description\"\n class=\"w-full\"\n :"
},
{
"path": "src/components/newtab/workflow/edit/EditNotification.vue",
"chars": 2207,
"preview": "<template>\n <div>\n <ui-textarea\n :model-value=\"data.description\"\n :placeholder=\"t('common.description')\"\n "
},
{
"path": "src/components/newtab/workflow/edit/EditParameterPrompt.vue",
"chars": 1246,
"preview": "<template>\n <div>\n <ui-textarea\n :model-value=\"data.description\"\n :placeholder=\"t('common.description')\"\n "
},
{
"path": "src/components/newtab/workflow/edit/EditPressKey.vue",
"chars": 4317,
"preview": "<template>\n <div>\n <ui-textarea\n :model-value=\"data.description\"\n class=\"w-full\"\n :placeholder=\"t('co"
},
{
"path": "src/components/newtab/workflow/edit/EditProxy.vue",
"chars": 1986,
"preview": "<template>\n <div>\n <ui-textarea\n :model-value=\"data.description\"\n :placeholder=\"t('common.description')\"\n "
},
{
"path": "src/components/newtab/workflow/edit/EditRegexVariable.vue",
"chars": 3101,
"preview": "<template>\n <div>\n <ui-textarea\n :model-value=\"data.description\"\n :placeholder=\"t('common.description')\"\n "
},
{
"path": "src/components/newtab/workflow/edit/EditSaveAssets.vue",
"chars": 3242,
"preview": "<template>\n <edit-interaction-base\n :data=\"data\"\n :hide=\"!permission.has.downloads\"\n :hide-selector=\"data.type"
},
{
"path": "src/components/newtab/workflow/edit/EditScrollElement.vue",
"chars": 1973,
"preview": "<template>\n <edit-interaction-base v-bind=\"{ data, hide: hideBase }\" @change=\"updateData\">\n <div v-if=\"!data.scrollI"
},
{
"path": "src/components/newtab/workflow/edit/EditSliceVariable.vue",
"chars": 1549,
"preview": "<template>\n <div>\n <ui-textarea\n :model-value=\"data.description\"\n :placeholder=\"t('common.description')\"\n "
},
{
"path": "src/components/newtab/workflow/edit/EditSortData.vue",
"chars": 3529,
"preview": "<template>\n <div>\n <ui-textarea\n :model-value=\"data.description\"\n :placeholder=\"t('common.description')\"\n "
},
{
"path": "src/components/newtab/workflow/edit/EditSwitchTab.vue",
"chars": 3347,
"preview": "<template>\n <div>\n <ui-textarea\n :model-value=\"data.description\"\n :placeholder=\"t('common.description')\"\n "
},
{
"path": "src/components/newtab/workflow/edit/EditSwitchTo.vue",
"chars": 1708,
"preview": "<template>\n <div class=\"space-y-2\">\n <ui-textarea\n :model-value=\"data.description\"\n :placeholder=\"t('commo"
},
{
"path": "src/components/newtab/workflow/edit/EditTabURL.vue",
"chars": 1799,
"preview": "<template>\n <div>\n <ui-textarea\n :model-value=\"data.description\"\n :placeholder=\"t('common.description')\"\n "
},
{
"path": "src/components/newtab/workflow/edit/EditTakeScreenshot.vue",
"chars": 4530,
"preview": "<template>\n <div class=\"take-screenshot\">\n <ui-textarea\n :model-value=\"data.description\"\n :placeholder=\"t("
},
{
"path": "src/components/newtab/workflow/edit/EditTrigger.vue",
"chars": 2323,
"preview": "<template>\n <div class=\"trigger\">\n <ui-textarea\n :model-value=\"data.description\"\n autoresize\n :placeh"
},
{
"path": "src/components/newtab/workflow/edit/EditTriggerEvent.vue",
"chars": 3684,
"preview": "<template>\n <edit-interaction-base v-bind=\"{ data, hide: hideBase }\" @change=\"updateData\">\n <ui-select\n :model-"
},
{
"path": "src/components/newtab/workflow/edit/EditUploadFile.vue",
"chars": 2866,
"preview": "<template>\n <edit-interaction-base\n class=\"mb-8\"\n v-bind=\"{ data, hide: hideBase }\"\n @change=\"updateData\"\n >\n"
},
{
"path": "src/components/newtab/workflow/edit/EditWaitConnections.vue",
"chars": 1570,
"preview": "<template>\n <div class=\"mb-4\">\n <ui-textarea\n :model-value=\"data.description\"\n class=\"w-full\"\n :place"
},
{
"path": "src/components/newtab/workflow/edit/EditWebhook.vue",
"chars": 6177,
"preview": "<template>\n <div class=\"mb-2 mt-4\">\n <ui-textarea\n :model-value=\"data.description\"\n :placeholder=\"t('commo"
},
{
"path": "src/components/newtab/workflow/edit/EditWhileLoop.vue",
"chars": 2661,
"preview": "<template>\n <div>\n <ui-textarea\n :model-value=\"data.description\"\n :placeholder=\"t('common.description')\"\n "
},
{
"path": "src/components/newtab/workflow/edit/EditWorkflowParameters.vue",
"chars": 7155,
"preview": "<template>\n <div\n class=\"scroll overflow-auto\"\n style=\"max-height: calc(100vh - 15rem); min-height: 200px\"\n >\n "
},
{
"path": "src/components/newtab/workflow/edit/EditWorkflowState.vue",
"chars": 4757,
"preview": "<template>\n <div>\n <ui-textarea\n :model-value=\"data.description\"\n class=\"w-full\"\n :placeholder=\"t('co"
},
{
"path": "src/components/newtab/workflow/edit/InsertWorkflowData.vue",
"chars": 2791,
"preview": "<template>\n <template v-if=\"variables\">\n <ui-checkbox\n :model-value=\"data.assignVariable\"\n block\n cla"
},
{
"path": "src/components/newtab/workflow/edit/Parameter/ParameterCheckboxValue.vue",
"chars": 494,
"preview": "<template>\n <ui-checkbox\n :model-value=\"Boolean(modelValue)\"\n type=\"text\"\n class=\"w-full\"\n :placeholder=\"pa"
},
{
"path": "src/components/newtab/workflow/edit/Parameter/ParameterInputOptions.vue",
"chars": 3416,
"preview": "<template>\n <div class=\"flex items-center\">\n <label class=\"flex items-center\">\n <ui-switch v-model=\"options.use"
},
{
"path": "src/components/newtab/workflow/edit/Parameter/ParameterInputValue.vue",
"chars": 984,
"preview": "<template>\n <ui-input\n :model-value=\"modelValue\"\n :mask=\"mask\"\n type=\"text\"\n class=\"w-full\"\n :placeholde"
},
{
"path": "src/components/newtab/workflow/edit/Parameter/ParameterJsonValue.vue",
"chars": 569,
"preview": "<template>\n <label>\n <span v-if=\"!editor\" class=\"ml-1 text-sm text-gray-600 dark:text-gray-200\">\n {{ paramData."
},
{
"path": "src/components/newtab/workflow/edit/Trigger/TriggerContextMenu.vue",
"chars": 2825,
"preview": "<template>\n <div>\n <template v-if=\"!permission.has[permissionName]\">\n <p>\n {{ t('workflow.blocks.trigger"
},
{
"path": "src/components/newtab/workflow/edit/Trigger/TriggerCronJob.vue",
"chars": 1477,
"preview": "<template>\n <ui-input\n :model-value=\"data.expression\"\n :label=\"t('workflow.blocks.trigger.forms.cron-expression')"
},
{
"path": "src/components/newtab/workflow/edit/Trigger/TriggerDate.vue",
"chars": 1086,
"preview": "<template>\n <div>\n <ui-input\n :model-value=\"data.date\"\n :max=\"maxDate\"\n :min=\"minDate\"\n :placeho"
},
{
"path": "src/components/newtab/workflow/edit/Trigger/TriggerElementChange.vue",
"chars": 3362,
"preview": "<template>\n <div>\n <ui-input\n v-model=\"observeDetail.matchPattern\"\n :label=\"t('workflow.blocks.trigger.ele"
},
{
"path": "src/components/newtab/workflow/edit/Trigger/TriggerElementOptions.vue",
"chars": 2215,
"preview": "<template>\n <ul class=\"space-y-2\">\n <li v-for=\"option in types\" :key=\"option\" class=\"group\">\n <ui-checkbox\n "
},
{
"path": "src/components/newtab/workflow/edit/Trigger/TriggerInterval.vue",
"chars": 1275,
"preview": "<template>\n <div class=\"flex items-center\">\n <ui-input\n :model-value=\"data.interval\"\n :label=\"t('workflow."
},
{
"path": "src/components/newtab/workflow/edit/Trigger/TriggerKeyboardShortcut.vue",
"chars": 2378,
"preview": "<template>\n <div>\n <div class=\"mb-2 flex items-center\">\n <ui-input\n :model-value=\"getReadableShortcut(re"
},
{
"path": "src/components/newtab/workflow/edit/Trigger/TriggerSpecificDay.vue",
"chars": 5314,
"preview": "<template>\n <div>\n <ui-popover\n :options=\"{ animation: null }\"\n trigger-width\n class=\"mb-2 w-full\"\n "
},
{
"path": "src/components/newtab/workflow/edit/Trigger/TriggerVisitWeb.vue",
"chars": 794,
"preview": "<template>\n <div>\n <ui-input\n :model-value=\"data.url\"\n :placeholder=\"t('workflow.blocks.trigger.forms.url'"
},
{
"path": "src/components/newtab/workflow/edit/TriggerEvent/TriggerEventInput.vue",
"chars": 794,
"preview": "<template>\n <div class=\"grid grid-cols-2 gap-2\">\n <ui-input v-model=\"defaultParams.data\" label=\"Data\" />\n <ui-inp"
},
{
"path": "src/components/newtab/workflow/edit/TriggerEvent/TriggerEventKeyboard.vue",
"chars": 1752,
"preview": "<template>\n <div class=\"grid grid-cols-2 gap-2\">\n <ui-checkbox\n v-for=\"item in ['altKey', 'ctrlKey', 'metaKey',"
},
{
"path": "src/components/newtab/workflow/edit/TriggerEvent/TriggerEventMouse.vue",
"chars": 2168,
"preview": "<template>\n <div class=\"grid grid-cols-2 gap-2\">\n <ui-checkbox\n v-for=\"item in ['altKey', 'ctrlKey', 'metaKey',"
},
{
"path": "src/components/newtab/workflow/edit/TriggerEvent/TriggerEventTouch.vue",
"chars": 878,
"preview": "<template>\n <div class=\"grid grid-cols-2 gap-2\">\n <ui-checkbox\n v-for=\"item in ['altKey', 'ctrlKey', 'metaKey',"
},
{
"path": "src/components/newtab/workflow/edit/TriggerEvent/TriggerEventWheel.vue",
"chars": 1008,
"preview": "<template>\n <div class=\"grid grid-cols-2 gap-2\">\n <ui-input\n v-model.number=\"defaultParams.deltaX\"\n type=\""
},
{
"path": "src/components/newtab/workflow/editor/EditorAddPackage.vue",
"chars": 2817,
"preview": "<template>\n <div class=\"flex items-center\">\n <ui-popover v-tooltip:bottom=\"t('packages.icon')\" class=\"mr-2\">\n <"
},
{
"path": "src/components/newtab/workflow/editor/EditorCustomEdge.vue",
"chars": 1658,
"preview": "<template>\n <base-edge\n :id=\"id\"\n :style=\"style\"\n :path=\"path[0]\"\n :marker-end=\"markerEnd\"\n :label=\"labe"
},
{
"path": "src/components/newtab/workflow/editor/EditorDebugging.vue",
"chars": 6823,
"preview": "<template>\n <ui-card\n v-if=\"workflowState?.state\"\n class=\"shadow-xl flex items-start fixed bottom-8 z-50 left-1/2"
},
{
"path": "src/components/newtab/workflow/editor/EditorLocalActions.vue",
"chars": 22959,
"preview": "<template>\n <span\n v-if=\"isTeam && workflow.tag\"\n :class=\"tagColors[workflow.tag]\"\n class=\"mr-2 rounded-md p-1"
},
{
"path": "src/components/newtab/workflow/editor/EditorLocalCtxMenu.vue",
"chars": 5432,
"preview": "<template>\n <ui-popover\n v-model=\"state.show\"\n :options=\"state.position\"\n padding=\"p-3\"\n @close=\"clearConte"
},
{
"path": "src/components/newtab/workflow/editor/EditorLocalSavedBlocks.vue",
"chars": 6011,
"preview": "<template>\n <div class=\"absolute bottom-0 z-50 w-full p-4\">\n <ui-card class=\"h-full w-full\" padding=\"p-0\">\n <di"
},
{
"path": "src/components/newtab/workflow/editor/EditorLogs.vue",
"chars": 1611,
"preview": "<template>\n <div\n v-if=\"(!logs || logs.length === 0) && workflowStates.length === 0\"\n class=\"text-center\"\n >\n "
},
{
"path": "src/components/newtab/workflow/editor/EditorPkgActions.vue",
"chars": 6990,
"preview": "<template>\n <ui-card\n v-if=\"userStore.user\"\n class=\"pointer-events-auto mr-2 space-x-1\"\n padding=\"p-1\"\n >\n "
},
{
"path": "src/components/newtab/workflow/editor/EditorSearchBlocks.vue",
"chars": 4835,
"preview": "<template>\n <div\n class=\"ml-2 inline-flex items-center rounded-lg bg-white dark:bg-gray-800\"\n >\n <button\n v"
},
{
"path": "src/components/newtab/workflow/editor/EditorUsedCredentials.vue",
"chars": 3335,
"preview": "<template>\n <ui-card\n v-if=\"credentials.length > 0\"\n padding=\"p-1\"\n class=\"pointer-events-auto mr-4\"\n >\n <"
},
{
"path": "src/components/newtab/workflow/settings/SettingsBlocks.vue",
"chars": 1232,
"preview": "<template>\n <div class=\"flex items-center\">\n <div class=\"mr-4 flex-1\">\n <p>\n {{ t('workflow.settings.blo"
},
{
"path": "src/components/newtab/workflow/settings/SettingsEvents.vue",
"chars": 7344,
"preview": "<template>\n <div>\n <div class=\"flex items-center\">\n <p class=\"flex-1\">{{ t('workflow.events.description') }}</p"
},
{
"path": "src/components/newtab/workflow/settings/SettingsGeneral.vue",
"chars": 6397,
"preview": "<template>\n <div class=\"flex items-center\">\n <div class=\"mr-4 flex-1\">\n <p>\n {{ t('workflow.settings.onE"
},
{
"path": "src/components/newtab/workflow/settings/SettingsTable.vue",
"chars": 1158,
"preview": "<template>\n <div class=\"flex items-center\">\n <div class=\"grow\">\n <p>\n {{ t('workflow.settings.defaultCol"
},
{
"path": "src/components/newtab/workflow/settings/event/EventCodeAction.vue",
"chars": 449,
"preview": "<template>\n <shared-codemirror\n :model-value=\"data.code\"\n class=\"h-full w-full\"\n @change=\"$emit('update:data',"
},
{
"path": "src/components/newtab/workflow/settings/event/EventCodeHTTP.vue",
"chars": 2680,
"preview": "<template>\n <div class=\"flex items-center gap-2\">\n <ui-select\n :model-value=\"data.method\"\n @change=\"emitDa"
},
{
"path": "src/components/newtab/workflows/WorkflowsFolder.vue",
"chars": 5239,
"preview": "<template>\n <div class=\"mt-6 border-t pt-4\">\n <div class=\"flex items-center text-gray-600 dark:text-gray-300\">\n "
},
{
"path": "src/components/newtab/workflows/WorkflowsHosted.vue",
"chars": 1737,
"preview": "<template>\n <shared-card\n v-for=\"workflow in workflows\"\n :key=\"workflow.hostId\"\n :data=\"workflow\"\n :menu=\"m"
},
{
"path": "src/components/newtab/workflows/WorkflowsLocal.vue",
"chars": 11222,
"preview": "<template>\n <div\n v-if=\"workflowStore.getWorkflows.length === 0\"\n class=\"md:flex items-center md:text-left text-c"
},
{
"path": "src/components/newtab/workflows/WorkflowsLocalCard.vue",
"chars": 3411,
"preview": "<template>\n <shared-card\n :data=\"workflow\"\n :data-workflow=\"workflow.id\"\n draggable=\"true\"\n class=\"local-wo"
},
{
"path": "src/components/newtab/workflows/WorkflowsShared.vue",
"chars": 1550,
"preview": "<template>\n <div\n v-if=\"workflows.length === 0\"\n class=\"md:flex items-center md:text-left text-center py-12\"\n >\n"
},
{
"path": "src/components/newtab/workflows/WorkflowsUserTeam.vue",
"chars": 5311,
"preview": "<template>\n <p v-if=\"!userStore.user\" class=\"my-4 text-center\">\n <ui-spinner v-if=\"!userStore.retrieved\" color=\"text"
},
{
"path": "src/components/popup/home/HomeSelectBlock.vue",
"chars": 3114,
"preview": "<template>\n <div class=\"px-4 pb-4\">\n <div class=\"mt-4 flex items-center\">\n <button @click=\"$emit('goBack')\">\n "
},
{
"path": "src/components/popup/home/HomeStartRecording.vue",
"chars": 3442,
"preview": "<template>\n <ui-tabs\n v-model=\"state.activeTab\"\n fill\n class=\"mx-4\"\n @change=\"$emit('update', $event)\"\n >\n"
},
{
"path": "src/components/popup/home/HomeTeamWorkflows.vue",
"chars": 2493,
"preview": "<template>\n <div class=\"space-y-2 px-5 pb-5\">\n <ui-card\n v-for=\"workflow in workflows\"\n :key=\"workflow.id\""
},
{
"path": "src/components/popup/home/HomeWorkflowCard.vue",
"chars": 2641,
"preview": "<template>\n <ui-card\n class=\"flex w-full items-center space-x-2 hover:ring-2 hover:ring-gray-900\"\n >\n <div\n "
},
{
"path": "src/components/transitions/TransitionExpand.vue",
"chars": 1463,
"preview": "<script>\nimport { h, Transition, TransitionGroup } from 'vue';\n\n/* eslint-disable */\nexport default {\n props: {\n gro"
},
{
"path": "src/components/transitions/TransitionSlide.vue",
"chars": 1413,
"preview": "<script>\nimport { h, Transition, TransitionGroup } from 'vue';\n\nexport default {\n props: {\n group: Boolean,\n dire"
},
{
"path": "src/components/ui/UiAutocomplete.vue",
"chars": 9181,
"preview": "<template>\n <ui-popover\n :id=\"componentId\"\n v-model=\"state.showPopover\"\n :class=\"{ block }\"\n :padding=\"`p-2"
}
]
// ... and 315 more files (download for full content)
About this extraction
This page contains the full source code of the AutomaApp/automa GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 515 files (2.0 MB), approximately 558.2k tokens, and a symbol index with 550 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.