diff --git a/.env.example b/.env.example index c3d6dd50..1f7b2938 100644 --- a/.env.example +++ b/.env.example @@ -8,17 +8,15 @@ VITE_PUBLIC_NAME=__MSG_EXT_NAME_DEV__ # ------------------- Extension KEY=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxO2GOH63WJB07mAFB2/pfkIYPg27J4Scwyv/Z1P/hl0iRB9kCSFBRrYkt+vX9CToZvE/MmwExxFWxhAXpvz0yQFmB8trUQxP/ubc6TePQQ5NiuE6bg+qlfLqaNPfNojJfZZSCe5HYSVEGvW1hxRhiXfRCy26NyRu/Y6D7CPCHfhkGDoGLExpVW1zEqfUcRPBh5cedlwFxFtAbd1OzmZnPllMOW+AMBP6GqlybkGuxpys4xt+32lVq3kb36LogdudQRe9geq9qKnk0tFNiqMe5QR04c0+IRuq3IGHLre+4peViG2w7bBgGY0r4ZxQ1/sOvoeh5XGdKPQKFF61Io5hqwIDAQAB -UNINSTALL_URL=https://forms.gle/49dAyTL1pLNEcvu76 # -------------------- Web App INLINE_RUNTIME_CHUNK=false # Extension VITE_PUBLIC_CHROME_EXTENSION_ID=blfahkipmbikdeipdaogkjggbmgbippe -VITE_PUBLIC_EDGE_EXTENSION_ID=blfahkipmbikdeipdaogkjggbmgbippe # i18n -VITE_PUBLIC_I18N=http://localhost:3000/locales +VITE_PUBLIC_I18N=/locales # Firebase VITE_PUBLIC_FIREBASE_API_KEY= diff --git a/.github/instructions/nx.instructions.md b/.github/instructions/nx.instructions.md index f7f50acb..b450f393 100644 --- a/.github/instructions/nx.instructions.md +++ b/.github/instructions/nx.instructions.md @@ -4,17 +4,19 @@ applyTo: '**' // This file is automatically generated by Nx Console -You are in an nx workspace using Nx 21.2.3 and npm as the package manager. +You are in an nx workspace using Nx 21.3.11 and npm as the package manager. You have access to the Nx MCP server and the tools it provides. Use them. Follow these guidelines in order to best help the user: # General Guidelines + - When answering questions, use the nx_workspace tool first to gain an understanding of the workspace architecture - For questions around nx configuration, best practices or if you're unsure, use the nx_docs tool to get relevant, up-to-date docs!! Always use this instead of assuming things about nx configuration - If the user needs help with an Nx configuration or project graph error, use the 'nx_workspace' tool to get any errors - To help answer questions about the workspace structure or simply help with demonstrating how tasks depend on each other, use the 'nx_visualize_graph' tool # Generation Guidelines + If the user wants to generate something, use the following flow: - learn about the nx workspace and any specifics the user needs by using the 'nx_workspace' tool and the 'nx_project_details' tool if applicable @@ -29,19 +31,20 @@ If the user wants to generate something, use the following flow: - use the information provided in the log file to answer the user's question or continue with what they were doing # Running Tasks Guidelines + If the user wants help with tasks or commands (which include keywords like "test", "build", "lint", or other similar actions), use the following flow: + - Use the 'nx_current_running_tasks_details' tool to get the list of tasks (this can include tasks that were completed, stopped or failed). - If there are any tasks, ask the user if they would like help with a specific task then use the 'nx_current_running_task_output' tool to get the terminal output for that task/command - Use the terminal output from 'nx_current_running_task_output' to see what's wrong and help the user fix their problem. Use the appropriate tools if necessary - If the user would like to rerun the task or command, always use `nx run ` to rerun in the terminal. This will ensure that the task will run in the nx context and will be run the same way it originally executed -- If the task was marked as "continuous" do not offer to rerun the task. This task is already running and the user can see the output in the terminal. You can use 'nx_current_running_task_output' to get the output of the task to verify the output. - +- If the task was marked as "continuous" do not offer to rerun the task. This task is already running and the user can see the output in the terminal. You can use 'nx_current_running_task_output' to get the output of the task to verify the output. # CI Error Guidelines + If the user wants help with fixing an error in their CI pipeline, use the following flow: + - Retrieve the list of current CI Pipeline Executions (CIPEs) using the 'nx_cloud_cipe_details' tool - If there are any errors, use the 'nx_cloud_fix_cipe_failure' tool to retrieve the logs for a specific task - Use the task logs to see what's wrong and help the user fix their problem. Use the appropriate tools if necessary - Make sure that the problem is fixed by running the task that you passed into the 'nx_cloud_fix_cipe_failure' tool - - diff --git a/.verdaccio/config.yml b/.verdaccio/config.yml deleted file mode 100644 index f74420f2..00000000 --- a/.verdaccio/config.yml +++ /dev/null @@ -1,28 +0,0 @@ -# path to a directory with all packages -storage: ../tmp/local-registry/storage - -# a list of other known repositories we can talk to -uplinks: - npmjs: - url: https://registry.npmjs.org/ - maxage: 60m - -packages: - '**': - # give all users (including non-authenticated users) full access - # because it is a local registry - access: $all - publish: $all - unpublish: $all - - # if package is not available locally, proxy requests to npm registry - proxy: npmjs - -# log settings -log: - type: stdout - format: pretty - level: warn - -publish: - allow_offline: true # set offline to true to allow publish offline diff --git a/DISCUSSION.md b/DISCUSSION.md deleted file mode 100644 index 6d14697a..00000000 --- a/DISCUSSION.md +++ /dev/null @@ -1,45 +0,0 @@ -# How to Find the Element - -Here’s the no-fluff, fast-track guide to inspecting and finding elements in Chrome/Edge/Firefox on both Mac and Windows: - -βΈ» - -πŸ”Ž For Mac - -1. Open Browser β†’ Launch Chrome/Edge/Firefox. -2. Right-click Element β†’ On the page, right-click the element you want β†’ select Inspect. - β€’ Shortcut: Cmd + Option + I opens DevTools. - β€’ Cmd + Shift + C lets you directly pick an element by hovering. -3. Locate Element in DOM β†’ The Elements panel shows HTML. Hover over tags to highlight on the page. -4. Check Attributes β†’ Look at id, class, name, data-* attributes. -5. Copy Selector/XPath (optional): Right-click highlighted node β†’ Copy β†’ choose Copy selector or Copy XPath. - -βΈ» - -πŸ”Ž For Windows - -1. Open Browser β†’ Same deal, launch Chrome/Edge/Firefox. -2. Right-click Element β†’ On the page, right-click β†’ choose Inspect. - β€’ Shortcut: Ctrl + Shift + I opens DevTools. - β€’ Ctrl + Shift + C activates element picker. -3. Locate in DOM β†’ HTML structure appears in the Elements panel; hovering highlights page sections. -4. Check Attributes β†’ Identify useful selectors like id, class, name. -5. Copy Selector/XPath β†’ Right-click the DOM node β†’ Copy β†’ Copy selector or Copy XPath. - -βΈ» - -βœ… Pro tip (Mac & Windows): - -β€’ Use the Console to test selectors: - -```javascript -document.querySelector('your-selector') -``` - -or - -```javascript -$x('//xpath') -``` - -β€’ For dynamic pages, always check if the element updates/reloads (watch for Detached from DOM in DevTools). \ No newline at end of file diff --git a/apps/acf-extension/project.json b/apps/acf-extension/project.json index f27f1a1d..f8927c94 100644 --- a/apps/acf-extension/project.json +++ b/apps/acf-extension/project.json @@ -12,8 +12,7 @@ "cwd": "apps/acf-extension", "args": ["--node-env=production", "--watch"] }, - "continuous": true, - "dependsOn": ["watch-deps"] + "continuous": true } } } diff --git a/apps/acf-extension/src/content_scripts/action.ts b/apps/acf-extension/src/content_scripts/action.ts index 2b5f65ba..7a2e20e1 100644 --- a/apps/acf-extension/src/content_scripts/action.ts +++ b/apps/acf-extension/src/content_scripts/action.ts @@ -22,7 +22,7 @@ const ActionProcessor = (() => { const process = async (action: IAction) => { const elementFinder = await ACFValue.getValue(action.elementFinder); const elements = await Common.start(elementFinder, action.settings); - if (elements === undefined) { + if (elements === undefined || elements.length === 0) { throw EActionStatus.SKIPPED; } const value = action.value ? await ACFValue.getValue(action.value, action.settings) : action.value; diff --git a/apps/acf-extension/src/content_scripts/actions.ts b/apps/acf-extension/src/content_scripts/actions.ts index 231f17e5..1867328d 100644 --- a/apps/acf-extension/src/content_scripts/actions.ts +++ b/apps/acf-extension/src/content_scripts/actions.ts @@ -52,9 +52,9 @@ const Actions = (() => { await statusBar.wait(action.initWait, STATUS_BAR_TYPE.ACTION_WAIT); await AddonProcessor.check(action.addon, action.settings); if (action.type === 'userscript') { - action.status = await UserScriptProcessor.start(action as IUserScript); + action.status = await UserScriptProcessor.start(action); } else { - action.status = await ActionProcessor.start(action as IAction); + action.status = await ActionProcessor.start(action); } notify(action); } catch (error) { diff --git a/apps/acf-extension/src/content_scripts/common.ts b/apps/acf-extension/src/content_scripts/common.ts index e93b9753..d260d219 100644 --- a/apps/acf-extension/src/content_scripts/common.ts +++ b/apps/acf-extension/src/content_scripts/common.ts @@ -3,6 +3,7 @@ import { SettingsStorage } from '@dhruv-techapps/acf-store'; import { ConfigError } from '@dhruv-techapps/core-common'; import { I18N_ERROR } from './i18n'; import { statusBar } from './status-bar'; +import DomWatchManager from './util/dom-watch-manager'; const Common = (() => { const retryFunc = async (retry?: number, retryInterval?: number | string) => { @@ -15,6 +16,19 @@ const Common = (() => { return false; }; + const setElementProcessed = (elements: Array) => { + elements.forEach((element) => { + element.setAttribute('data-acf-processed', 'true'); + }); + }; + + const filterProcessedElements = (elements: Array): Array => { + return elements.filter((element) => { + // Add your filtering logic here + return !element.hasAttribute('data-acf-processed'); + }); + }; + const getElements = async (document: Document, elementFinder: string, retry: number, retryInterval: number | string): Promise | undefined> => { let elements: HTMLElement[] | undefined; try { @@ -132,6 +146,12 @@ const Common = (() => { if (!elements || elements.length === 0) { return checkRetryOption(retryOption, elementFinder, retryGoto); } + + if (DomWatchManager.getStatus().isActive && elements) { + elements = filterProcessedElements(elements); + } + + setElementProcessed(elements); return elements; }; diff --git a/apps/acf-extension/src/content_scripts/config.ts b/apps/acf-extension/src/content_scripts/config.ts index c856a8a7..5ba77ef5 100644 --- a/apps/acf-extension/src/content_scripts/config.ts +++ b/apps/acf-extension/src/content_scripts/config.ts @@ -9,11 +9,13 @@ import { GoogleAnalyticsService } from '@dhruv-techapps/shared-google-analytics' import { GoogleSheetsCS } from '@dhruv-techapps/shared-google-sheets'; import { STATUS_BAR_TYPE } from '@dhruv-techapps/shared-status-bar'; import { scope } from '../common/instrument'; +import Actions from './actions'; import BatchProcessor from './batch'; import Common from './common'; import { Hotkey } from './hotkey'; import { I18N_COMMON } from './i18n'; import { statusBar } from './status-bar'; +import DomWatchManager from './util/dom-watch-manager'; import GoogleSheets from './util/google-sheets'; const CONFIG_I18N = { @@ -45,6 +47,20 @@ const ConfigProcessor = (() => { return events; }; + const InitializeDomWatcher = (config: IConfiguration) => { + // If watch settings are provided and enabled, set up DOM watcher for the entire configuration + if (config.watch?.watchEnabled) { + // Set up the sequence restart callback for DOM watcher + DomWatchManager.setSequenceRestartCallback(async () => { + console.debug(`Actions: Restarting entire action sequence due to DOM changes`); + await Actions.start(config.actions, window.ext.__batchRepeat + 1); + }); + + // Register the configuration-level DOM watcher after actions complete initially + DomWatchManager.registerConfiguration(config.watch); + } + }; + const start = async (config: IConfiguration) => { try { window.ext.__sessionCount = new Session(config.id).getCount(); @@ -53,7 +69,9 @@ const ConfigProcessor = (() => { } const sheets = GoogleSheets.getSheets(config); window.ext.__sheets = await new GoogleSheetsCS().getValues(sheets, config.spreadsheetId); + // Clear any existing DOM watchers before starting new actions await BatchProcessor.start(config.actions, config.batch); + InitializeDomWatcher(config); const { notifications } = await new SettingsStorage().getSettings(); if (notifications) { const { onConfig, sound, discord } = notifications; diff --git a/apps/acf-extension/src/content_scripts/index.ts b/apps/acf-extension/src/content_scripts/index.ts index c72ebf62..d7cd3d94 100644 --- a/apps/acf-extension/src/content_scripts/index.ts +++ b/apps/acf-extension/src/content_scripts/index.ts @@ -66,6 +66,7 @@ self.onerror = (...rest) => { chrome.runtime.onMessage.addListener(async (message) => { const { action, configId } = message; + if (action === RUNTIME_MESSAGE_ACF.RUN_CONFIG) { try { new ConfigStorage().getConfigById(configId).then(async (config) => { diff --git a/apps/acf-extension/src/content_scripts/util/dom-watch-manager.ts b/apps/acf-extension/src/content_scripts/util/dom-watch-manager.ts new file mode 100644 index 00000000..e18144a4 --- /dev/null +++ b/apps/acf-extension/src/content_scripts/util/dom-watch-manager.ts @@ -0,0 +1,176 @@ +import { IWatchSettings, defaultWatchSettings } from '@dhruv-techapps/acf-common'; + +interface DomWatchState { + isActive: boolean; + observer: MutationObserver | null; + watchSettings: IWatchSettings | null; + debounceTimeout: number | null; + currentUrl: string; + startTime: number; + sequenceRestartCallback?: () => Promise; +} + +const DomWatchManager = (() => { + const state: DomWatchState = { + isActive: false, + observer: null, + watchSettings: null, + debounceTimeout: null, + currentUrl: window.location.href, + startTime: 0, + sequenceRestartCallback: undefined + }; + + // Process added nodes and check if any match actions + const processAddedNodes = async (addedNodes: NodeList): Promise => { + if (!state.watchSettings || !state.sequenceRestartCallback) { + return; + } + + console.debug('DomWatchManager: Restarting action sequence due to DOM changes'); + await state.sequenceRestartCallback(); + }; + + // Debounced processing + const debounceProcessing = (processingFn: () => Promise, delay: number): void => { + // Clear existing timeout + if (state.debounceTimeout) { + clearTimeout(state.debounceTimeout); + } + + // Set new timeout + state.debounceTimeout = window.setTimeout(async () => { + try { + await processingFn(); + } catch (error) { + console.error('DomWatchManager: Error in debounced processing:', error); + } + state.debounceTimeout = null; + }, delay); + }; + + // Check lifecycle stop conditions + const shouldStopWatching = (): boolean => { + if (!state.watchSettings?.lifecycleStopConditions) return false; + + const { lifecycleStopConditions } = state.watchSettings; + + // Check timeout + if (lifecycleStopConditions.timeout) { + const elapsed = Date.now() - state.startTime; + if (elapsed >= lifecycleStopConditions.timeout * 60 * 1000) { + // Convert mins to milliseconds + console.debug('DomWatchManager: Stopping due to timeout'); + return true; + } + } + + // Check URL change + if (lifecycleStopConditions.urlChange && state.currentUrl !== window.location.href) { + console.debug('DomWatchManager: Stopping due to URL change'); + return true; + } + + return false; + }; + + // Mutation observer callback + const handleMutations = (mutations: MutationRecord[]): void => { + if (!state.isActive || !state.watchSettings) { + return; + } + + // Check if we should stop watching + if (shouldStopWatching()) { + return; + } + + for (const mutation of mutations) { + if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { + // Debounce the processing to avoid excessive restarts + debounceProcessing( + () => processAddedNodes(mutation.addedNodes), + (state.watchSettings.debounce || 1) * 1000 // Convert seconds to milliseconds + ); + } + } + }; + + // Initialize the mutation observer + const initializeObserver = (): void => { + if (state.observer || !state.watchSettings) { + return; + } + + const watchRoot = state.watchSettings.watchRootSelector || 'body'; + const nodeList = document.querySelectorAll(watchRoot); + const rootElements = nodeList.length > 0 ? nodeList : [document.body]; + + state.observer = new MutationObserver(handleMutations); + rootElements.forEach((rootElement) => { + state.observer?.observe(rootElement, { + childList: true, + subtree: true, + attributes: !!state.watchSettings?.watchAttributes, // track attributes + attributeFilter: state.watchSettings?.watchAttributes // optional optimization + }); + }); + + console.debug(`DomWatchManager: Initialized observer on ${watchRoot}`); + }; + + // Register configuration-level DOM watching + const registerConfiguration = (watchSettings: IWatchSettings): void => { + if (!watchSettings.watchEnabled) { + return; + } + + const mergedSettings: IWatchSettings = { + ...defaultWatchSettings, + ...watchSettings + }; + + state.watchSettings = mergedSettings; + state.startTime = Date.now(); + if (!state.isActive) { + start(); + } + + console.debug(`DomWatchManager: Registered configuration-level DOM watching`); + }; + + // Start DOM watching + const start = (): void => { + if (state.isActive || !state.watchSettings) { + return; + } + + state.isActive = true; + state.currentUrl = window.location.href; + initializeObserver(); + + console.debug('DomWatchManager: Started configuration-level DOM watching'); + }; + + // Get current watch status + const getStatus = () => ({ + isActive: state.isActive, + watchEnabled: state.watchSettings?.watchEnabled || false, + startTime: state.startTime, + settings: state.watchSettings + }); + + // Set the callback function for sequence restart + const setSequenceRestartCallback = (callback: () => Promise): void => { + state.sequenceRestartCallback = callback; + }; + + return { + registerConfiguration, + setSequenceRestartCallback, + start, + getStatus + }; +})(); + +export default DomWatchManager; diff --git a/apps/acf-extension/tsconfig.tsbuildinfo b/apps/acf-extension/tsconfig.tsbuildinfo new file mode 100644 index 00000000..2e3c79fd --- /dev/null +++ b/apps/acf-extension/tsconfig.tsbuildinfo @@ -0,0 +1 @@ +{"fileNames":[],"fileInfos":[],"root":[],"options":{"composite":true,"declarationMap":true,"emitDeclarationOnly":true,"importHelpers":true,"module":99,"noEmitOnError":true,"noFallthroughCasesInSwitch":true,"noImplicitOverride":true,"noImplicitReturns":true,"noUnusedLocals":true,"skipLibCheck":true,"strict":true,"target":9},"version":"5.9.2"} \ No newline at end of file diff --git a/apps/acf-options-page/project.json b/apps/acf-options-page/project.json index abff8f64..a64733eb 100644 --- a/apps/acf-options-page/project.json +++ b/apps/acf-options-page/project.json @@ -11,8 +11,7 @@ { "projects": ["acf-extension"], "target": "serve" - }, - "watch-deps" + } ] } } diff --git a/apps/acf-options-page/src/app/configs/action/index.tsx b/apps/acf-options-page/src/app/configs/action/index.tsx index 18c51395..f385fa99 100644 --- a/apps/acf-options-page/src/app/configs/action/index.tsx +++ b/apps/acf-options-page/src/app/configs/action/index.tsx @@ -1,5 +1,5 @@ import { useTimeout } from '@acf-options-page/_hooks/message.hooks'; -import { addAction, addUserscript, selectedConfigSelector, switchBatchModal } from '@acf-options-page/store/config'; +import { addAction, addUserscript, openWatchModalAPI, selectedConfigSelector, switchBatchModal } from '@acf-options-page/store/config'; import { actionSelector, setActionMessage } from '@acf-options-page/store/config/action/action.slice'; import { useAppDispatch, useAppSelector } from '@acf-options-page/store/hooks'; import { Button, Card, Col, Row } from 'react-bootstrap'; @@ -45,10 +45,13 @@ function Action() { - + - diff --git a/apps/acf-options-page/src/app/configs/configs.tsx b/apps/acf-options-page/src/app/configs/configs.tsx index 76a9ef0f..69d0d26b 100644 --- a/apps/acf-options-page/src/app/configs/configs.tsx +++ b/apps/acf-options-page/src/app/configs/configs.tsx @@ -9,7 +9,7 @@ import { IConfiguration } from '@dhruv-techapps/acf-common'; import { ChangeEvent, createRef, useEffect } from 'react'; import { Alert, Col, Container, Row } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import { BatchModal, ConfigSettingsModal, RemoveConfigsModal, ReorderConfigsModal, ScheduleModal } from '../../modal'; +import { BatchModal, ConfigSettingsModal, RemoveConfigsModal, ReorderConfigsModal, ScheduleModal, WatchModal } from '../../modal'; import Footer from '../footer'; import Action from './action'; import Config from './config'; @@ -99,6 +99,7 @@ function Configs(props: Readonly) {