From 79578836778f51d29e6c9d6a4fac61761b89f7fe Mon Sep 17 00:00:00 2001 From: Ahmad Kholid Date: Mon, 9 May 2022 14:43:44 +0800 Subject: [PATCH] feat: add element selector when record workflow --- src/assets/css/tailwind.css | 2 +- src/background/index.js | 5 +- .../content/selector/SelectorBlocks.vue} | 10 +- .../content/selector/SelectorElementList.vue} | 0 .../selector/SelectorElementsDetail.vue} | 14 +- .../content/selector/SelectorQuery.vue} | 0 .../shared/SharedElementHighlighter.vue | 95 +++ .../popup/home/HomeSelectBlock.vue} | 100 +-- .../popup/home/HomeStartRecording.vue | 106 ++++ .../transitions/TransitionSlide.vue | 63 ++ src/content/elementSelector/App.vue | 154 ++--- .../elementSelector/AppElementHighlighter.vue | 46 -- src/content/elementSelector/compsUi.js | 4 - src/content/elementSelector/icons.js | 98 --- src/content/elementSelector/index.js | 35 +- .../workflow/WorkflowAddBlock.vue | 3 - .../elementSelector/workflow/WorkflowList.vue | 69 --- src/content/injectAppStyles.js | 40 ++ src/content/services/recordWorkflow/App.vue | 585 ++++++++++++++++++ .../services/recordWorkflow/addBlock.js | 15 + src/content/services/recordWorkflow/icons.js | 8 + src/content/services/recordWorkflow/index.js | 23 + src/content/services/recordWorkflow/main.js | 34 + .../recordEvents.js} | 141 +++-- src/locales/en/popup.json | 8 +- src/popup/pages/Home.vue | 137 ++-- src/popup/pages/Recording.vue | 55 +- src/popup/pages/workflow/Edit.vue | 3 - src/utils/shared.js | 13 + webpack.config.js | 3 +- 30 files changed, 1338 insertions(+), 531 deletions(-) rename src/{content/elementSelector/AppBlocks.vue => components/content/selector/SelectorBlocks.vue} (88%) rename src/{content/elementSelector/AppElementList.vue => components/content/selector/SelectorElementList.vue} (100%) rename src/{content/elementSelector/AppElementsDetail.vue => components/content/selector/SelectorElementsDetail.vue} (91%) rename src/{content/elementSelector/AppSelector.vue => components/content/selector/SelectorQuery.vue} (100%) create mode 100644 src/components/content/shared/SharedElementHighlighter.vue rename src/{content/elementSelector/workflow/WorkflowEditor.vue => components/popup/home/HomeSelectBlock.vue} (57%) create mode 100644 src/components/popup/home/HomeStartRecording.vue create mode 100644 src/components/transitions/TransitionSlide.vue delete mode 100644 src/content/elementSelector/AppElementHighlighter.vue delete mode 100644 src/content/elementSelector/workflow/WorkflowAddBlock.vue delete mode 100644 src/content/elementSelector/workflow/WorkflowList.vue create mode 100644 src/content/injectAppStyles.js create mode 100644 src/content/services/recordWorkflow/App.vue create mode 100644 src/content/services/recordWorkflow/addBlock.js create mode 100644 src/content/services/recordWorkflow/icons.js create mode 100644 src/content/services/recordWorkflow/index.js create mode 100644 src/content/services/recordWorkflow/main.js rename src/content/services/{recordWorkflow.js => recordWorkflow/recordEvents.js} (63%) delete mode 100644 src/popup/pages/workflow/Edit.vue diff --git a/src/assets/css/tailwind.css b/src/assets/css/tailwind.css index 7fcb301f7..f78fc6812 100644 --- a/src/assets/css/tailwind.css +++ b/src/assets/css/tailwind.css @@ -32,7 +32,7 @@ @apply dark:border-gray-700; } -:host, body { +body { font-family: 'Inter var'; font-size: 16px; font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11'; diff --git a/src/background/index.js b/src/background/index.js index a23ac8af6..1488a563f 100644 --- a/src/background/index.js +++ b/src/background/index.js @@ -196,7 +196,8 @@ browser.webNavigation.onCommitted.addListener( const lastFlow = recording.flows[recording.flows.length - 1]; const isClickSubmit = - lastFlow.id === 'event-click' && transitionType === 'form_submit'; + lastFlow.id === 'event-click' && + (transitionType === 'form_submit' || lastFlow.isClickLink); if (isClickSubmit) return; @@ -235,11 +236,11 @@ browser.tabs.onActivated.addListener(async ({ tabId }) => { recording.activeTab = { id, url }; recording.flows.push({ id: 'switch-tab', + description: title, data: { url, matchPattern: url, createIfNoMatch: true, - description: title || url, }, }); }); diff --git a/src/content/elementSelector/AppBlocks.vue b/src/components/content/selector/SelectorBlocks.vue similarity index 88% rename from src/content/elementSelector/AppBlocks.vue rename to src/components/content/selector/SelectorBlocks.vue index 8cf37745a..bc0999b75 100644 --- a/src/content/elementSelector/AppBlocks.vue +++ b/src/components/content/selector/SelectorBlocks.vue @@ -39,11 +39,11 @@ 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 '../blocksHandler/handlerForms'; -import handleGetText from '../blocksHandler/handlerGetText'; -import handleEventClick from '../blocksHandler/handlerEventClick'; -import handelTriggerEvent from '../blocksHandler/handlerTriggerEvent'; -import handleElementScroll from '../blocksHandler/handlerElementScroll'; +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: { diff --git a/src/content/elementSelector/AppElementList.vue b/src/components/content/selector/SelectorElementList.vue similarity index 100% rename from src/content/elementSelector/AppElementList.vue rename to src/components/content/selector/SelectorElementList.vue diff --git a/src/content/elementSelector/AppElementsDetail.vue b/src/components/content/selector/SelectorElementsDetail.vue similarity index 91% rename from src/content/elementSelector/AppElementsDetail.vue rename to src/components/content/selector/SelectorElementsDetail.vue index dd6f58818..655c746c7 100644 --- a/src/content/elementSelector/AppElementsDetail.vue +++ b/src/components/content/selector/SelectorElementsDetail.vue @@ -15,7 +15,7 @@ style="max-height: calc(100vh - 17rem)" > - @@ -39,10 +39,10 @@ /> - + - - diff --git a/src/content/elementSelector/workflow/WorkflowEditor.vue b/src/components/popup/home/HomeSelectBlock.vue similarity index 57% rename from src/content/elementSelector/workflow/WorkflowEditor.vue rename to src/components/popup/home/HomeSelectBlock.vue index b55dd5d59..87229a30f 100644 --- a/src/content/elementSelector/workflow/WorkflowEditor.vue +++ b/src/components/popup/home/HomeSelectBlock.vue @@ -1,64 +1,63 @@ diff --git a/src/content/elementSelector/App.vue b/src/content/elementSelector/App.vue index b7b618ca5..42967f5f1 100644 --- a/src/content/elementSelector/App.vue +++ b/src/content/elementSelector/App.vue @@ -5,15 +5,16 @@ 'bg-black bg-opacity-30': !state.hide, }" class="root fixed h-full w-full pointer-events-none top-0 text-black left-0" + style="z-index: 99999999" >
Automa

-
- - - - - - +
+ + +
- - - - + :disabled="state.hide" + :data="elementsHighlightData" + :items="{ + hoveredElements: state.hoveredElements, + selectedElements: state.selectedElements, + }" + @update="state[$event.key] = $event.items" + /> + +
+
diff --git a/src/content/elementSelector/compsUi.js b/src/content/elementSelector/compsUi.js index be84c58e6..d80f10ef7 100644 --- a/src/content/elementSelector/compsUi.js +++ b/src/content/elementSelector/compsUi.js @@ -1,12 +1,10 @@ import VAutofocus from '@/directives/VAutofocus'; import UiTab from '@/components/ui/UiTab.vue'; -import UiList from '@/components/ui/UiList.vue'; import UiTabs from '@/components/ui/UiTabs.vue'; import UiInput from '@/components/ui/UiInput.vue'; import UiButton from '@/components/ui/UiButton.vue'; import UiSelect from '@/components/ui/UiSelect.vue'; import UiExpand from '@/components/ui/UiExpand.vue'; -import UiListItem from '@/components/ui/UiListItem.vue'; import UiTextarea from '@/components/ui/UiTextarea.vue'; import UiCheckbox from '@/components/ui/UiCheckbox.vue'; import UiTabPanel from '@/components/ui/UiTabPanel.vue'; @@ -16,12 +14,10 @@ import TransitionExpand from '@/components/transitions/TransitionExpand.vue'; export default function (app) { app.component('UiTab', UiTab); app.component('UiTabs', UiTabs); - app.component('UiList', UiList); app.component('UiInput', UiInput); app.component('UiButton', UiButton); app.component('UiSelect', UiSelect); app.component('UiExpand', UiExpand); - app.component('UiListItem', UiListItem); app.component('UiTextarea', UiTextarea); app.component('UiCheckbox', UiCheckbox); app.component('UiTabPanel', UiTabPanel); diff --git a/src/content/elementSelector/icons.js b/src/content/elementSelector/icons.js index c12daaba1..ce00dd6a9 100644 --- a/src/content/elementSelector/icons.js +++ b/src/content/elementSelector/icons.js @@ -1,125 +1,27 @@ import { riEyeLine, - riAB, - riLink, - riStopLine, - riFlowChart, - riParagraph, - riMouseLine, - riEarthLine, - riImageLine, - riChat3Line, riCheckLine, riCloseLine, - riTimerLine, - riWindowLine, - riFocus3Line, - riGithubFill, riEyeOffLine, - riGlobalLine, - riCursorLine, - riWindow2Line, - riRepeat2Line, - riRefreshFill, - riRefreshLine, - riRestartLine, - riTwitterLine, - riDiscordLine, - riCommandLine, - riSearch2Line, - riBracketsLine, riFileCopyLine, riDragMoveLine, - riFileTextLine, - riCalendarLine, - riDownloadLine, - riLightbulbLine, - riFolderZipLine, - riClipboardLine, - riEqualizerLine, - riDatabase2Line, riListUnordered, riArrowLeftLine, - riCodeSSlashLine, - riFileUploadLine, - riDeleteBin7Line, - riTimerFlashLine, - riFlashlightLine, riArrowLeftSLine, - riArrowGoBackLine, - riCloseCircleLine, - riInputCursorMove, - riArrowUpDownLine, riInformationLine, - riFileDownloadLine, - riShieldKeyholeLine, riArrowDropDownLine, - riArrowLeftRightLine, - riArrowGoForwardLine, - riLightbulbFlashLine, } from 'v-remixicon/icons'; export default { - riAB, - riLink, riEyeLine, - riStopLine, - riFlowChart, - riParagraph, - riMouseLine, - riEarthLine, - riImageLine, - riChat3Line, riCheckLine, riCloseLine, - riTimerLine, - riFocus3Line, - riGithubFill, riEyeOffLine, - riGlobalLine, - riWindowLine, - riCursorLine, - riWindow2Line, - riRepeat2Line, - riRefreshFill, - riRefreshLine, - riRestartLine, - riTwitterLine, - riDiscordLine, - riCommandLine, - riSearch2Line, - riBracketsLine, riFileCopyLine, riDragMoveLine, - riFileTextLine, - riCalendarLine, - riDownloadLine, - riLightbulbLine, - riFolderZipLine, - riClipboardLine, - riEqualizerLine, - riDatabase2Line, riListUnordered, riArrowLeftLine, - riCodeSSlashLine, - riFileUploadLine, - riDeleteBin7Line, - riTimerFlashLine, - riFlashlightLine, riArrowLeftSLine, - riArrowGoBackLine, - riCloseCircleLine, - riInputCursorMove, - riArrowUpDownLine, riInformationLine, - riFileDownloadLine, - riShieldKeyholeLine, riArrowDropDownLine, - riArrowLeftRightLine, - riArrowGoForwardLine, - riLightbulbFlashLine, - mdiGoogleSheet: - 'M19,11V9H11V5H9V9H5V11H9V19H11V11H19M19,3C19.5,3 20,3.2 20.39,3.61C20.8,4 21,4.5 21,5V19C21,19.5 20.8,20 20.39,20.39C20,20.8 19.5,21 19,21H5C4.5,21 4,20.8 3.61,20.39C3.2,20 3,19.5 3,19V5C3,4.5 3.2,4 3.61,3.61C4,3.2 4.5,3 5,3H19Z', - mdiCursorDefaultClickOutline: - 'M11.5,11L17.88,16.37L17,16.55L16.36,16.67C15.73,16.8 15.37,17.5 15.65,18.07L15.92,18.65L17.28,21.59L15.86,22.25L14.5,19.32L14.24,18.74C13.97,18.15 13.22,17.97 12.72,18.38L12.21,18.78L11.5,19.35V11M10.76,8.69A0.76,0.76 0 0,0 10,9.45V20.9C10,21.32 10.34,21.66 10.76,21.66C10.95,21.66 11.11,21.6 11.24,21.5L13.15,19.95L14.81,23.57C14.94,23.84 15.21,24 15.5,24C15.61,24 15.72,24 15.83,23.92L18.59,22.64C18.97,22.46 19.15,22 18.95,21.63L17.28,18L19.69,17.55C19.85,17.5 20,17.43 20.12,17.29C20.39,16.97 20.35,16.5 20,16.21L11.26,8.86L11.25,8.87C11.12,8.76 10.95,8.69 10.76,8.69M15,10V8H20V10H15M13.83,4.76L16.66,1.93L18.07,3.34L15.24,6.17L13.83,4.76M10,0H12V5H10V0M3.93,14.66L6.76,11.83L8.17,13.24L5.34,16.07L3.93,14.66M3.93,3.34L5.34,1.93L8.17,4.76L6.76,6.17L3.93,3.34M7,10H2V8H7V10', }; diff --git a/src/content/elementSelector/index.js b/src/content/elementSelector/index.js index b1ef7002f..636964e71 100644 --- a/src/content/elementSelector/index.js +++ b/src/content/elementSelector/index.js @@ -1,33 +1,6 @@ import browser from 'webextension-polyfill'; import initElementSelector from './main'; - -function generateStyleEl(css, classes = true) { - const style = document.createElement('style'); - style.textContent = css; - - if (classes) { - style.classList.add('automa-element-selector'); - } - - return style; -} -async function injectAppStyles(appRoot) { - try { - const response = await fetch( - browser.runtime.getURL('/elementSelector.css') - ); - const mainCSS = await response.text(); - const appStyleEl = generateStyleEl(mainCSS, false); - appRoot.shadowRoot.appendChild(appStyleEl); - - const fontURL = browser.runtime.getURL('/Inter-roman-latin.var.woff2'); - const fontCSS = `@font-face { font-family: "Inter var"; font-weight: 100 900; font-display: swap; font-style: normal; font-named-instance: "Regular"; src: url("${fontURL}") format("woff2") }`; - const fontStyleEl = generateStyleEl(fontCSS); - document.head.appendChild(fontStyleEl); - } catch (error) { - console.error(error); - } -} +import injectAppStyles from '../injectAppStyles'; function elementSelectorInstance() { const rootElementExist = document.querySelector( @@ -64,14 +37,10 @@ function elementSelectorInstance() { rootElement.classList.add('automa-element-selector'); rootElement.attachShadow({ mode: 'open' }); - const automaCSS = `.automa-element-selector { pointer-events: none; direction: ltr } \n [automa-isDragging] { user-select: none } \n [automa-el-list] {outline: 2px dashed #6366f1;}`; - const automaStyleEl = generateStyleEl(automaCSS); - initElementSelector(rootElement); - await injectAppStyles(rootElement); + await injectAppStyles(rootElement.shadowRoot); document.documentElement.appendChild(rootElement); - document.documentElement.appendChild(automaStyleEl); } catch (error) { console.error(error); } diff --git a/src/content/elementSelector/workflow/WorkflowAddBlock.vue b/src/content/elementSelector/workflow/WorkflowAddBlock.vue deleted file mode 100644 index 8163301c7..000000000 --- a/src/content/elementSelector/workflow/WorkflowAddBlock.vue +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/src/content/elementSelector/workflow/WorkflowList.vue b/src/content/elementSelector/workflow/WorkflowList.vue deleted file mode 100644 index dd32aa452..000000000 --- a/src/content/elementSelector/workflow/WorkflowList.vue +++ /dev/null @@ -1,69 +0,0 @@ - - diff --git a/src/content/injectAppStyles.js b/src/content/injectAppStyles.js new file mode 100644 index 000000000..18307660f --- /dev/null +++ b/src/content/injectAppStyles.js @@ -0,0 +1,40 @@ +import browser from 'webextension-polyfill'; + +export function generateStyleEl(css, classes = true) { + const style = document.createElement('style'); + style.textContent = css; + + if (classes) { + style.classList.add('automa-element-selector'); + } + + return style; +} + +export default async function (appRoot, customCss = '') { + try { + const response = await fetch( + browser.runtime.getURL('/elementSelector.css') + ); + const mainCSS = await response.text(); + const appStyleEl = generateStyleEl(mainCSS + customCss, false); + appRoot.appendChild(appStyleEl); + + const fontStyleExists = document.head.querySelector( + '.automa-element-selector' + ); + + if (!fontStyleExists) { + const commonCSS = + '\n.automa-element-selector { direction: ltr } \n [automa-isDragging] { user-select: none } \n [automa-el-list] {outline: 2px dashed #6366f1;}'; + + const fontURL = browser.runtime.getURL('/Inter-roman-latin.var.woff2'); + const fontCSS = `@font-face { font-family: "Inter var"; font-weight: 100 900; font-display: swap; font-style: normal; font-named-instance: "Regular"; src: url("${fontURL}") format("woff2") }`; + const fontStyleEl = generateStyleEl(fontCSS + commonCSS); + + document.head.appendChild(fontStyleEl); + } + } catch (error) { + console.error(error); + } +} diff --git a/src/content/services/recordWorkflow/App.vue b/src/content/services/recordWorkflow/App.vue new file mode 100644 index 000000000..fd942e437 --- /dev/null +++ b/src/content/services/recordWorkflow/App.vue @@ -0,0 +1,585 @@ + + diff --git a/src/content/services/recordWorkflow/addBlock.js b/src/content/services/recordWorkflow/addBlock.js new file mode 100644 index 000000000..ecda67afe --- /dev/null +++ b/src/content/services/recordWorkflow/addBlock.js @@ -0,0 +1,15 @@ +import browser from 'webextension-polyfill'; + +export default async function (detail) { + const { isRecording, recording } = await browser.storage.local.get([ + 'isRecording', + 'recording', + ]); + + if (!isRecording || !recording) return; + + if (typeof detail === 'function') detail(recording); + else recording.flows.push(detail); + + await browser.storage.local.set({ recording }); +} diff --git a/src/content/services/recordWorkflow/icons.js b/src/content/services/recordWorkflow/icons.js new file mode 100644 index 000000000..46e83bc4b --- /dev/null +++ b/src/content/services/recordWorkflow/icons.js @@ -0,0 +1,8 @@ +import { riRecordCircleLine, riArrowLeftLine } from 'v-remixicon/icons'; + +export default { + riRecordCircleLine, + riArrowLeftLine, + mdiDragHorizontal: + 'M3,15V13H5V15H3M3,11V9H5V11H3M7,15V13H9V15H7M7,11V9H9V11H7M11,15V13H13V15H11M11,11V9H13V11H11M15,15V13H17V15H15M15,11V9H17V11H15M19,15V13H21V15H19M19,11V9H21V11H19Z', +}; diff --git a/src/content/services/recordWorkflow/index.js b/src/content/services/recordWorkflow/index.js new file mode 100644 index 000000000..3f7a4dc49 --- /dev/null +++ b/src/content/services/recordWorkflow/index.js @@ -0,0 +1,23 @@ +import browser from 'webextension-polyfill'; +import initElementSelector from './main'; +import initRecordEvents from './recordEvents'; + +(async () => { + try { + const element = document.querySelector('#automa-recording'); + if (element) return; + + const destroyRecordEvents = await initRecordEvents(); + const elementSelectorInstance = await initElementSelector(); + + browser.runtime.onMessage.addListener(function messageListener({ type }) { + if (type === 'recording:stop') { + destroyRecordEvents(); + elementSelectorInstance.unmount(); + browser.runtime.onMessage.removeListener(messageListener); + } + }); + } catch (error) { + console.error(error); + } +})(); diff --git a/src/content/services/recordWorkflow/main.js b/src/content/services/recordWorkflow/main.js new file mode 100644 index 000000000..6184fda69 --- /dev/null +++ b/src/content/services/recordWorkflow/main.js @@ -0,0 +1,34 @@ +import { createApp } from 'vue'; +import vRemixicon from 'v-remixicon'; +import App from './App.vue'; +import icons from './icons'; +import injectAppStyles from '../../injectAppStyles'; + +const customCSS = ` + #app { + font-family: 'Inter var'; + line-height: 1.5; + } + .content { + width: 250px; + } +`; + +export default function () { + const rootElement = document.createElement('div'); + rootElement.attachShadow({ mode: 'open' }); + rootElement.setAttribute('id', 'automa-recording'); + rootElement.classList.add('automa-element-selector'); + document.body.appendChild(rootElement); + + return injectAppStyles(rootElement.shadowRoot, customCSS).then(() => { + const appRoot = document.createElement('div'); + appRoot.setAttribute('id', 'app'); + rootElement.shadowRoot.appendChild(appRoot); + + const app = createApp(App).use(vRemixicon, icons); + app.mount(appRoot); + + return app; + }); +} diff --git a/src/content/services/recordWorkflow.js b/src/content/services/recordWorkflow/recordEvents.js similarity index 63% rename from src/content/services/recordWorkflow.js rename to src/content/services/recordWorkflow/recordEvents.js index 59ef32781..f6c36f3ad 100644 --- a/src/content/services/recordWorkflow.js +++ b/src/content/services/recordWorkflow/recordEvents.js @@ -1,25 +1,18 @@ -import { getCssSelector } from 'css-selector-generator'; +import { finder } from '@medv/finder'; +import { nanoid } from 'nanoid'; import browser from 'webextension-polyfill'; import { debounce } from '@/utils/helper'; +import addBlock from './addBlock'; +const isAutomaInstance = (target) => + target.id === 'automa-recording' || + document.body.hasAttribute('automa-selecting'); const textFieldEl = (el) => ['INPUT', 'TEXTAREA'].includes(el.tagName) || el.isContentEditable; -async function addBlock(detail) { - const { isRecording, recording } = await browser.storage.local.get([ - 'isRecording', - 'recording', - ]); - - if (!isRecording || !recording) return; - - if (typeof detail === 'function') detail(recording); - else recording.flows.push(detail); - - await browser.storage.local.set({ recording }); -} - function changeListener({ target }) { + if (isAutomaInstance(target)) return; + const isInputEl = target.tagName === 'INPUT'; const inputType = target.getAttribute('type'); const execludeInput = isInputEl && ['checkbox', 'radio'].includes(inputType); @@ -27,14 +20,14 @@ function changeListener({ target }) { if (execludeInput) return; let block = null; - const selector = getCssSelector(target); + const selector = finder(target); const isSelectEl = target.tagName === 'SELECT'; const elementName = target.ariaLabel || target.name; if (isInputEl && inputType === 'file') { block = { id: 'upload-file', - description: elementName || selector, + description: elementName, data: { selector, waitForSelector: true, @@ -43,16 +36,22 @@ function changeListener({ target }) { }, }; } else if (textFieldEl(target) || isSelectEl) { + let description = ''; + + if (elementName && elementName.length < 12) { + description = `${isSelectEl ? 'Select' : 'Text field'} (${elementName})`; + } + block = { id: 'forms', data: { selector, delay: 100, + description, clearValue: true, value: target.value, waitForSelector: true, type: isSelectEl ? 'select' : 'text-field', - description: `${isSelectEl ? 'Select' : 'Text field'} (${elementName})`, }, }; } else { @@ -60,11 +59,11 @@ function changeListener({ target }) { id: 'trigger-event', data: { selector, + description, eventName: 'change', eventType: 'event', waitForSelector: true, eventParams: { bubbles: true }, - description: `Change event (${selector})`, }, }; } @@ -90,34 +89,51 @@ function keyEventListener({ type, repeat, }) { - const isTextField = textFieldEl(target); + if (isAutomaInstance(target)) return; + const isTextField = textFieldEl(target); if (isTextField) return; - const selector = getCssSelector(target); + const selector = finder(target); - addBlock({ - id: 'trigger-event', - data: { - selector, - eventName: type, - eventType: 'keyboard-event', - eventParams: { - key, - code, - repeat, - altKey, - ctrlKey, - metaKey, - keyCode, - shiftKey, + addBlock((recording) => { + const lastFlow = recording.flows.at(-1); + const block = { + id: 'trigger-event', + data: { + selector, + eventName: type, + eventType: 'keyboard-event', + eventParams: { + key, + code, + repeat, + altKey, + ctrlKey, + metaKey, + keyCode, + shiftKey, + }, + description: `${type}: ${key === ' ' ? 'Space' : key}`, }, - description: `${type}(${key === ' ' ? 'Space' : key}): ${selector}`, - }, + }; + + if (lastFlow.id === 'trigger-event') { + if (!lastFlow.groupId) lastFlow.groupId = nanoid(); + + block.groupId = lastFlow.groupId; + } + + recording.flows.push(block); + + return recording; }); } function clickListener(event) { const { target } = event; + + if (isAutomaInstance(target)) return; + let isClickLink = true; const isTextField = (target.tagName === 'INPUT' && target.getAttribute('type') === 'text') || @@ -125,7 +141,7 @@ function clickListener(event) { if (isTextField) return; - const selector = getCssSelector(target); + const selector = finder(target); if (target.tagName === 'A') { if (event.ctrlKey || event.metaKey) return; @@ -136,11 +152,14 @@ function clickListener(event) { if (openInNewTab) { event.preventDefault(); + const description = (target.innerText || target.href).slice(0, 24); + addBlock({ id: 'link', + description, data: { selector, - description: (target.innerText || target.href).slice(0, 64), + description, }, }); @@ -150,24 +169,29 @@ function clickListener(event) { } } - const elText = target.innerText || target.ariaLabel || target.title; + const elText = (target.innerText || target.ariaLabel || target.title).slice( + 0, + 24 + ); addBlock({ isClickLink, id: 'event-click', - description: elText.slice(0, 64) || selector, + description: elText, data: { selector, + description: elText, waitForSelector: true, - description: elText.slice(0, 64), }, }); } const scrollListener = debounce(({ target }) => { + if (isAutomaInstance(target)) return; + const isDocument = target === document; const element = isDocument ? document.documentElement : target; - const selector = isDocument ? 'html' : getCssSelector(target); + const selector = isDocument ? 'html' : finder(target); addBlock((recording) => { const lastFlow = recording.flows[recording.flows.length - 1]; @@ -183,7 +207,6 @@ const scrollListener = debounce(({ target }) => { recording.flows.push({ id: 'element-scroll', - description: selector, data: { selector, smooth: true, @@ -194,30 +217,24 @@ const scrollListener = debounce(({ target }) => { }); }, 500); -function cleanUp() { +export function cleanUp() { document.removeEventListener('click', clickListener, true); document.removeEventListener('change', changeListener, true); document.removeEventListener('scroll', scrollListener, true); document.removeEventListener('keyup', keyEventListener, true); document.removeEventListener('keydown', keyEventListener, true); } -function messageListener({ type }) { - if (type === 'recording:stop') { - cleanUp(); - browser.runtime.onMessage.removeListener(messageListener); - } -} -(async () => { +export default async function () { const { isRecording } = await browser.storage.local.get('isRecording'); - if (!isRecording) return; - - document.addEventListener('click', clickListener, true); - document.addEventListener('scroll', scrollListener, true); - document.addEventListener('change', changeListener, true); - document.addEventListener('keyup', keyEventListener, true); - document.addEventListener('keydown', keyEventListener, true); + if (isRecording) { + document.addEventListener('click', clickListener, true); + document.addEventListener('scroll', scrollListener, true); + document.addEventListener('change', changeListener, true); + document.addEventListener('keyup', keyEventListener, true); + document.addEventListener('keydown', keyEventListener, true); + } - browser.runtime.onMessage.addListener(messageListener); -})(); + return cleanUp; +} diff --git a/src/locales/en/popup.json b/src/locales/en/popup.json index 90717d111..7db39ee4a 100644 --- a/src/locales/en/popup.json +++ b/src/locales/en/popup.json @@ -7,7 +7,13 @@ "record": { "title": "Record workflow", "button": "Record", - "name": "Workflow name" + "name": "Workflow name", + "selectBlock": "Select a block to start from", + "anotherBlock": "Can't start from this block", + "tabs": { + "new": "New workflow", + "existing": "Existing workflow" + } }, "elementSelector": { "name": "Element selector", diff --git a/src/popup/pages/Home.vue b/src/popup/pages/Home.vue index 0634fac6a..be893a1a3 100644 --- a/src/popup/pages/Home.vue +++ b/src/popup/pages/Home.vue @@ -8,13 +8,13 @@ v-tooltip.group="t('home.record.title')" icon class="mr-2" - @click="recordWorkflow" + @click="state.newRecordingModal = true" >
+ + +
+

+ {{ t('home.record.title') }} +

+ +
+ +
+
+ diff --git a/src/popup/pages/Recording.vue b/src/popup/pages/Recording.vue index 5995d76c5..40417a58d 100644 --- a/src/popup/pages/Recording.vue +++ b/src/popup/pages/Recording.vue @@ -69,10 +69,10 @@ const state = reactive({ isGenerating: false, }); -function generateDrawflow() { +function generateDrawflow(startBlock, startBlockData) { let nextNodeId = nanoid(); - const triggerId = nanoid(); - let prevNodeId = triggerId; + const triggerId = startBlock.id || nanoid(); + let prevNodeId = startBlock.id || triggerId; const nodes = { [triggerId]: { @@ -80,7 +80,7 @@ function generateDrawflow() { pos_y: 300, inputs: {}, outputs: { - output_1: { + [startBlock ? startBlock.output : 'output_1']: { connections: [{ node: nextNodeId, output: 'input_1' }], }, }, @@ -90,9 +90,14 @@ function generateDrawflow() { class: 'trigger', html: 'BlockBasic', data: tasks.trigger.data, + ...startBlockData, }, }; - const position = { x: 260, y: 300 }; + + const position = { + y: startBlockData ? startBlockData.pos_y + 50 : 300, + x: startBlockData ? startBlockData.pos_x + 120 : 260, + }; state.flows.forEach((block, index) => { const node = { @@ -110,7 +115,7 @@ function generateDrawflow() { node.inputs.input_1.connections.push({ node: prevNodeId, - input: 'output_1', + input: index === 0 && startBlock ? startBlock.output : 'output_1', }); const isLastIndex = index === state.flows.length - 1; @@ -126,13 +131,15 @@ function generateDrawflow() { } const inNewRow = (index + 1) % 5 === 0; - const blockNameLen = tasks[block.id].name.length * 11 + 120; + const blockNameLen = tasks[block.id].name.length * 14 + 120; position.x = inNewRow ? 50 : position.x + blockNameLen; position.y = inNewRow ? position.y + 150 : position.y; nodes[node.id] = node; }); + if (startBlock) return nodes; + return { drawflow: { Home: { data: nodes } } }; } async function stopRecording() { @@ -145,14 +152,32 @@ async function stopRecording() { try { state.isGenerating = true; - const drawflow = generateDrawflow(); - - await Workflow.insert({ - data: { - name: state.name, - drawflow: JSON.stringify(drawflow), - }, - }); + if (state.workflowId) { + const workflow = Workflow.find(state.workflowId); + const drawflow = + typeof workflow.drawflow === 'string' + ? JSON.parse(workflow.drawflow) + : workflow.drawflow; + const node = drawflow.drawflow.Home.data[state.connectFrom.id]; + const updatedDrawflow = generateDrawflow(state.connectFrom, node); + + Object.assign(drawflow.drawflow.Home.data, updatedDrawflow); + + await Workflow.update({ + where: state.workflowId, + data: { + drawflow: JSON.stringify(drawflow), + }, + }); + } else { + const drawflow = generateDrawflow(); + await Workflow.insert({ + data: { + name: state.name, + drawflow: JSON.stringify(drawflow), + }, + }); + } await browser.storage.local.remove(['isRecording', 'recording']); await browser.browserAction.setBadgeText({ text: '' }); diff --git a/src/popup/pages/workflow/Edit.vue b/src/popup/pages/workflow/Edit.vue deleted file mode 100644 index 6890e581f..000000000 --- a/src/popup/pages/workflow/Edit.vue +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/src/utils/shared.js b/src/utils/shared.js index 49cf99375..01a06c502 100644 --- a/src/utils/shared.js +++ b/src/utils/shared.js @@ -1043,6 +1043,19 @@ export const communities = [ }, ]; +export const elementsHighlightData = { + selectedElements: { + stroke: '#2563EB', + activeStroke: '#f87171', + fill: 'rgba(37, 99, 235, 0.1)', + activeFill: 'rgba(248, 113, 113, 0.1)', + }, + hoveredElements: { + stroke: '#fbbf24', + fill: 'rgba(251, 191, 36, 0.1)', + }, +}; + export const conditionBuilder = { valueTypes: [ { diff --git a/webpack.config.js b/webpack.config.js index aedeab4d7..3004fe4f4 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -48,7 +48,8 @@ const options = { 'src', 'content', 'services', - 'recordWorkflow.js' + 'recordWorkflow', + 'index.js' ), shortcutListener: path.join( __dirname,