From 1ad4b5d97abd9fe7c8c3ed59e7f35f7561aa94f6 Mon Sep 17 00:00:00 2001 From: Rohit Date: Sun, 26 Jan 2025 18:51:54 +0530 Subject: [PATCH 001/119] feat: nth-child selector generation for child selectors --- server/src/workflow-management/selector.ts | 75 ++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 8a9096ec..3f44a01e 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -1654,6 +1654,31 @@ export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates } } + if (element.parentElement) { + // Look for identical siblings + const siblings = Array.from(element.parentElement.children); + const identicalSiblings = siblings.filter(sibling => { + if (sibling === element) return false; + + let siblingSelector = sibling.tagName.toLowerCase(); + const siblingClassName = typeof sibling.className === 'string' ? sibling.className : ''; + if (siblingClassName) { + const siblingClasses = siblingClassName.split(/\s+/).filter(Boolean); + const validSiblingClasses = siblingClasses.filter(cls => !cls.startsWith('!') && !cls.includes(':')); + if (validSiblingClasses.length > 0) { + siblingSelector += '.' + validSiblingClasses.map(cls => CSS.escape(cls)).join('.'); + } + } + + return siblingSelector === selector; + }); + + if (identicalSiblings.length > 0) { + const position = siblings.indexOf(element) + 1; + selector += `:nth-child(${position})`; + } + } + return selector; } @@ -1894,6 +1919,31 @@ export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates } } + if (element.parentElement) { + // Look for identical siblings + const siblings = Array.from(element.parentElement.children); + const identicalSiblings = siblings.filter(sibling => { + if (sibling === element) return false; + + let siblingSelector = sibling.tagName.toLowerCase(); + const siblingClassName = typeof sibling.className === 'string' ? sibling.className : ''; + if (siblingClassName) { + const siblingClasses = siblingClassName.split(/\s+/).filter(Boolean); + const validSiblingClasses = siblingClasses.filter(cls => !cls.startsWith('!') && !cls.includes(':')); + if (validSiblingClasses.length > 0) { + siblingSelector += '.' + validSiblingClasses.map(cls => CSS.escape(cls)).join('.'); + } + } + + return siblingSelector === selector; + }); + + if (identicalSiblings.length > 0) { + const position = siblings.indexOf(element) + 1; + selector += `:nth-child(${position})`; + } + } + return selector; } @@ -2025,6 +2075,31 @@ export const getChildSelectors = async (page: Page, parentSelector: string): Pro } } + if (element.parentElement) { + // Look for identical siblings + const siblings = Array.from(element.parentElement.children); + const identicalSiblings = siblings.filter(sibling => { + if (sibling === element) return false; + + let siblingSelector = sibling.tagName.toLowerCase(); + const siblingClassName = typeof sibling.className === 'string' ? sibling.className : ''; + if (siblingClassName) { + const siblingClasses = siblingClassName.split(/\s+/).filter(Boolean); + const validSiblingClasses = siblingClasses.filter(cls => !cls.startsWith('!') && !cls.includes(':')); + if (validSiblingClasses.length > 0) { + siblingSelector += '.' + validSiblingClasses.map(cls => CSS.escape(cls)).join('.'); + } + } + + return siblingSelector === selector; + }); + + if (identicalSiblings.length > 0) { + const position = siblings.indexOf(element) + 1; + selector += `:nth-child(${position})`; + } + } + return selector; } From 54f7deb3ede1b78754fe76669a16e9486fdf0e40 Mon Sep 17 00:00:00 2001 From: Rohit Date: Mon, 27 Jan 2025 15:00:19 +0530 Subject: [PATCH 002/119] feat: add retry mechanism, modularization --- maxun-core/src/interpret.ts | 420 ++++++++++++++++++++---------------- 1 file changed, 231 insertions(+), 189 deletions(-) diff --git a/maxun-core/src/interpret.ts b/maxun-core/src/interpret.ts index d87e2477..251dda6d 100644 --- a/maxun-core/src/interpret.ts +++ b/maxun-core/src/interpret.ts @@ -1,5 +1,5 @@ /* eslint-disable no-await-in-loop, no-restricted-syntax */ -import { Page, PageScreenshotOptions } from 'playwright'; +import { ElementHandle, Page, PageScreenshotOptions } from 'playwright'; import { PlaywrightBlocker } from '@cliqz/adblocker-playwright'; import fetch from 'cross-fetch'; import path from 'path'; @@ -548,232 +548,274 @@ export default class Interpreter extends EventEmitter { } } - private async handlePagination(page: Page, config: { listSelector: string, fields: any, limit?: number, pagination: any }) { + private async handlePagination(page: Page, config: { + listSelector: string, + fields: any, + limit?: number, + pagination: any +}) { let allResults: Record[] = []; let previousHeight = 0; - // track unique items per page to avoid re-scraping let scrapedItems: Set = new Set(); - let visitedUrls: string[] = []; + let visitedUrls: Set = new Set(); + const MAX_RETRIES = 3; + const RETRY_DELAY = 1000; // 1 second delay between retries - // Debug logging helper const debugLog = (message: string, ...args: any[]) => { - console.log(`[Page ${visitedUrls.length + 1}] ${message}`, ...args); + console.log(`[Page ${visitedUrls.size}] [URL: ${page.url()}] ${message}`, ...args); + }; + + const scrapeCurrentPage = async () => { + const results = await page.evaluate((cfg) => window.scrapeList(cfg), config); + const newResults = results.filter(item => { + const uniqueKey = JSON.stringify(item); + if (scrapedItems.has(uniqueKey)) return false; + scrapedItems.add(uniqueKey); + return true; + }); + allResults = allResults.concat(newResults); + debugLog("Results collected:", allResults.length); + }; + + const checkLimit = () => { + if (config.limit && allResults.length >= config.limit) { + allResults = allResults.slice(0, config.limit); + return true; + } + return false; + }; + + // Enhanced button finder with retry mechanism + const findWorkingButton = async (selectors: string[], retryCount = 0): Promise<{ + button: ElementHandle | null, + workingSelector: string | null + }> => { + for (const selector of selectors) { + try { + const button = await page.waitForSelector(selector, { + state: 'attached', + timeout: 10000 // Reduced timeout for faster checks + }); + if (button) { + debugLog('Found working selector:', selector); + return { button, workingSelector: selector }; + } + } catch (error) { + debugLog(`Selector failed: ${selector}`); + } + } + + // Implement retry mechanism when no selectors work + if (selectors.length > 0 && retryCount < MAX_RETRIES) { + debugLog(`Retry attempt ${retryCount + 1} of ${MAX_RETRIES}`); + await page.waitForTimeout(RETRY_DELAY); + return findWorkingButton(selectors, retryCount + 1); + } + + return { button: null, workingSelector: null }; + }; + + const retryOperation = async (operation: () => Promise, retryCount = 0): Promise => { + try { + return await operation(); + } catch (error) { + if (retryCount < MAX_RETRIES) { + debugLog(`Retrying operation. Attempt ${retryCount + 1} of ${MAX_RETRIES}`); + await page.waitForTimeout(RETRY_DELAY); + return retryOperation(operation, retryCount + 1); + } + debugLog(`Operation failed after ${MAX_RETRIES} retries`); + return false; + } }; let availableSelectors = config.pagination.selector.split(','); - while (true) { + try { + while (true) { + // Reduced timeout for faster performance + await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {}); + switch (config.pagination.type) { - case 'scrollDown': - await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); - await page.waitForTimeout(2000); + case 'scrollDown': + await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); + await page.waitForTimeout(2000); - const currentHeight = await page.evaluate(() => document.body.scrollHeight); - if (currentHeight === previousHeight) { - const finalResults = await page.evaluate((cfg) => window.scrapeList(cfg), config); - allResults = allResults.concat(finalResults); - return allResults; - } + const currentHeight = await page.evaluate(() => document.body.scrollHeight); + if (currentHeight === previousHeight) { + const finalResults = await page.evaluate((cfg) => window.scrapeList(cfg), config); + allResults = allResults.concat(finalResults); + return allResults; + } - previousHeight = currentHeight; - break; + previousHeight = currentHeight; + break; - case 'scrollUp': - await page.evaluate(() => window.scrollTo(0, 0)); - await page.waitForTimeout(2000); + case 'scrollUp': + await page.evaluate(() => window.scrollTo(0, 0)); + await page.waitForTimeout(2000); - const currentTopHeight = await page.evaluate(() => document.documentElement.scrollTop); - if (currentTopHeight === 0) { - const finalResults = await page.evaluate((cfg) => window.scrapeList(cfg), config); - allResults = allResults.concat(finalResults); - return allResults; - } + const currentTopHeight = await page.evaluate(() => document.documentElement.scrollTop); + if (currentTopHeight === 0) { + const finalResults = await page.evaluate((cfg) => window.scrapeList(cfg), config); + allResults = allResults.concat(finalResults); + return allResults; + } - previousHeight = currentTopHeight; - break; + previousHeight = currentTopHeight; + break; - case 'clickNext': - debugLog("Current URL:", page.url()); - const pageResults = await page.evaluate((cfg) => window.scrapeList(cfg), config); - - // Filter out already scraped items - const newResults = pageResults.filter(item => { - const uniqueKey = JSON.stringify(item); - if (scrapedItems.has(uniqueKey)) return false; - scrapedItems.add(uniqueKey); - return true; - }); - - allResults = allResults.concat(newResults); - debugLog("Results collected so far:", allResults.length); - - if (config.limit && allResults.length >= config.limit) { - return allResults.slice(0, config.limit); - } + case 'clickNext': { + const currentUrl = page.url(); + visitedUrls.add(currentUrl); + + await scrapeCurrentPage(); + if (checkLimit()) return allResults; - await page.waitForLoadState('networkidle', { timeout: 30000 }); - await page.waitForTimeout(2000); - - let checkButton = null; - let workingSelector = null; - - // Try each selector with explicit waiting - for (const selector of availableSelectors) { + const { button, workingSelector } = await findWorkingButton(availableSelectors); + if (!button || !workingSelector) { + // Final retry for navigation when no selectors work + const success = await retryOperation(async () => { try { - checkButton = await page.waitForSelector(selector, { - state: 'attached', - timeout: 30000 - }); - if (checkButton) { - workingSelector = selector; - debugLog('Found working selector:', selector); - break; - } - } catch (error) { - debugLog(`Selector failed: ${selector} - ${error.message}`); + await page.evaluate(() => window.history.forward()); + const newUrl = page.url(); + return !visitedUrls.has(newUrl); + } catch { + return false; } - } + }); + + if (!success) return allResults; + break; + } - if (!workingSelector) { - debugLog('No working selector found after trying all options'); - return allResults; - } + availableSelectors = availableSelectors.slice( + availableSelectors.indexOf(workingSelector) + ); - const nextButton = await page.$(workingSelector); - if (!nextButton) { - debugLog('Next button not found'); - return allResults; - } - - const selectorIndex = availableSelectors.indexOf(workingSelector); - availableSelectors = availableSelectors.slice(selectorIndex); + let retryCount = 0; + let navigationSuccess = false; + while (retryCount < MAX_RETRIES && !navigationSuccess) { try { - // Store current URL to check if navigation succeeded - const previousUrl = page.url(); - visitedUrls.push(previousUrl); - - // Try both click methods in sequence try { await Promise.all([ page.waitForNavigation({ - waitUntil: 'networkidle', - timeout: 15000 + waitUntil: 'networkidle', + timeout: 15000 }), - nextButton.click() + button.click() ]); - } catch (error) { - // If we're still on the same URL, try dispatch event - if (page.url() === previousUrl) { - await Promise.all([ - page.waitForNavigation({ - waitUntil: 'networkidle', - timeout: 15000 - }), - nextButton.dispatchEvent('click') - ]); - } - } - - await page.waitForLoadState('domcontentloaded'); - await page.waitForLoadState('networkidle', { timeout: 30000 }); - - const currentUrl = page.url(); - if (visitedUrls.includes(currentUrl)) { - debugLog(`Navigation failed/Detected navigation to previously visited URL: ${currentUrl}`); - return allResults; - } - - // Give the page a moment to stabilize after navigation - await page.waitForTimeout(1000); - - } catch (error) { - debugLog(`Navigation failed completely: ${error.message}`); - return allResults; - } - break; - - case 'clickLoadMore': - while (true) { - let checkButton = null; - let workingSelector = null; - - for (const selector of availableSelectors) { - try { - checkButton = await page.waitForSelector(selector, { - state: 'attached', - timeout: 30000 - }); - if (checkButton) { - workingSelector = selector; - debugLog('Found working selector:', selector); - break; - } - } catch (error) { - debugLog(`Load More selector failed: ${selector}`); - } - } - - if (!workingSelector) { - debugLog('No working Load More selector found'); - const finalResults = await page.evaluate((cfg) => window.scrapeList(cfg), config); - allResults = allResults.concat(finalResults); - return allResults; - } - - const loadMoreButton = await page.$(workingSelector); - if (!loadMoreButton) { - debugLog('Load More button not found'); - const finalResults = await page.evaluate((cfg) => window.scrapeList(cfg), config); - allResults = allResults.concat(finalResults); - return allResults; - } - - const selectorIndex = availableSelectors.indexOf(workingSelector); - availableSelectors = availableSelectors.slice(selectorIndex); - - try { - try { - await loadMoreButton.click(); - } catch (error) { - await loadMoreButton.dispatchEvent('click'); - } + navigationSuccess = true; } catch (error) { - const finalResults = await page.evaluate((cfg) => window.scrapeList(cfg), config); - allResults = allResults.concat(finalResults); - return allResults; + debugLog(`Regular click failed on attempt ${retryCount + 1}. Trying DispatchEvent`); + + // If regular click fails, try dispatchEvent + if (page.url() === currentUrl) { + try { + await Promise.all([ + page.waitForNavigation({ + waitUntil: 'networkidle', + timeout: 15000 + }), + button.dispatchEvent('click') + ]); + navigationSuccess = true; + } catch (dispatchError) { + debugLog(`DispatchEvent failed on attempt ${retryCount + 1}.`); + } + } } - await page.waitForTimeout(2000); - await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); - await page.waitForTimeout(2000); - - const currentHeight = await page.evaluate(() => document.body.scrollHeight); - if (currentHeight === previousHeight) { - debugLog('No more items loaded after Load More'); - const finalResults = await page.evaluate((cfg) => window.scrapeList(cfg), config); - allResults = allResults.concat(finalResults); - return allResults; + const newUrl = page.url(); + if (visitedUrls.has(newUrl)) { + debugLog(`Detected navigation to previously visited URL ${newUrl} on attempt ${retryCount + 1}`); + navigationSuccess = false; } - previousHeight = currentHeight; - - if (config.limit && allResults.length >= config.limit) { - allResults = allResults.slice(0, config.limit); - break; + + if (navigationSuccess) { + await page.waitForTimeout(1000); + } + } catch (error) { + debugLog(`Navigation attempt ${retryCount + 1} failed completely.`); + navigationSuccess = false; + } + + if (!navigationSuccess) { + retryCount++; + if (retryCount < MAX_RETRIES) { + debugLog(`Retrying navigation - attempt ${retryCount + 1} of ${MAX_RETRIES}`); + await page.waitForTimeout(RETRY_DELAY); } } - break; + } - default: - const results = await page.evaluate((cfg) => window.scrapeList(cfg), config); - allResults = allResults.concat(results); + if (!navigationSuccess) { + debugLog(`Navigation failed after ${MAX_RETRIES} attempts`); return allResults; + } + break; + } + + case 'clickLoadMore': { + while (true) { + const { button, workingSelector } = await findWorkingButton(availableSelectors); + if (!button || !workingSelector) { + // Final retry for load more when no selectors work + const success = await retryOperation(async () => { + await scrapeCurrentPage(); + return allResults.length > 0; + }); + + if (!success) return allResults; + break; + } + + availableSelectors = availableSelectors.slice( + availableSelectors.indexOf(workingSelector) + ); + + const loadMoreSuccess = await retryOperation(async () => { + try { + await button.click().catch(() => button.dispatchEvent('click')); + await page.waitForTimeout(1000); + + await page.evaluate(() => + window.scrollTo(0, document.body.scrollHeight) + ); + await page.waitForTimeout(1000); + + const currentHeight = await page.evaluate(() => + document.body.scrollHeight + ); + + if (currentHeight === previousHeight) { + await scrapeCurrentPage(); + return false; + } + previousHeight = currentHeight; + + return true; + } catch (error) { + await scrapeCurrentPage(); + return false; + } + }); + + if (!loadMoreSuccess || checkLimit()) return allResults; + } + } + + default: + await scrapeCurrentPage(); + return allResults; } - if (config.limit && allResults.length >= config.limit) { - allResults = allResults.slice(0, config.limit); - break; - } + if (checkLimit()) break; + } + } catch (error) { + debugLog(`Fatal error: ${error.message}`); + return allResults; } return allResults; From a07413df93fbbf5f6db37400d2868981abb8a98f Mon Sep 17 00:00:00 2001 From: Rohit Date: Tue, 28 Jan 2025 14:52:27 +0530 Subject: [PATCH 003/119] feat: rm general selector from chained selectors --- server/src/workflow-management/classes/Generator.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server/src/workflow-management/classes/Generator.ts b/server/src/workflow-management/classes/Generator.ts index 6e36f287..509c0177 100644 --- a/server/src/workflow-management/classes/Generator.ts +++ b/server/src/workflow-management/classes/Generator.ts @@ -759,8 +759,7 @@ export class WorkflowGenerator { selectors?.id, selectors?.hrefSelector, selectors?.accessibilitySelector, - selectors?.attrSelector, - selectors?.generalSelector + selectors?.attrSelector ] .filter(selector => selector !== null && selector !== undefined) .join(','); From 3c0f7900a4e05fa19693af87645a61e572bb153e Mon Sep 17 00:00:00 2001 From: Rohit Date: Wed, 29 Jan 2025 17:41:59 +0530 Subject: [PATCH 004/119] fix: register type actions correctly --- .../src/workflow-management/classes/Generator.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/server/src/workflow-management/classes/Generator.ts b/server/src/workflow-management/classes/Generator.ts index 6e36f287..a148e53b 100644 --- a/server/src/workflow-management/classes/Generator.ts +++ b/server/src/workflow-management/classes/Generator.ts @@ -22,7 +22,7 @@ import { getBestSelectorForAction } from "../utils"; import { browserPool } from "../../server"; import { uuid } from "uuidv4"; import { capture } from "../../utils/analytics" -import { encrypt } from "../../utils/auth"; +import { decrypt, encrypt } from "../../utils/auth"; interface PersistedGeneratedData { lastUsedSelector: string; @@ -1062,14 +1062,18 @@ export class WorkflowGenerator { if (condition.args && condition.args[1]) { if (!input.selector) { input.selector = condition.args[0]; + input.type = condition.args[2] } + if (input.selector === condition.args[0]) { input.actionCounter++; - if (condition.args[1].length === 1) { - input.value = input.value + condition.args[1]; - } else if (condition.args[1] === 'Backspace') { + const decryptedKey = decrypt(condition.args[1]); + + if (decryptedKey.length === 1) { + input.value += decryptedKey; + } else if (decryptedKey === 'Backspace') { input.value = input.value.slice(0, -1); - } else if (condition.args[1] !== 'Shift') { + } else if (decryptedKey !== 'Shift') { pushTheOptimizedAction(pair, index); pair.what.splice(index + 1, 0, { action: 'waitForLoadState', @@ -1081,7 +1085,7 @@ export class WorkflowGenerator { pushTheOptimizedAction(pair, index); input = { selector: condition.args[0], - value: condition.args[1], + value: decrypt(condition.args[1]), type: condition.args[2], actionCounter: 1, }; From 47a82fd30e99833f2caefa30174ebb7a45390148 Mon Sep 17 00:00:00 2001 From: Rohit Date: Wed, 29 Jan 2025 22:37:23 +0530 Subject: [PATCH 005/119] feat: add pagination for nested runs --- src/components/run/RunsTable.tsx | 82 ++++++++++++++++++++++++-------- 1 file changed, 63 insertions(+), 19 deletions(-) diff --git a/src/components/run/RunsTable.tsx b/src/components/run/RunsTable.tsx index 92233214..04634b6d 100644 --- a/src/components/run/RunsTable.tsx +++ b/src/components/run/RunsTable.tsx @@ -70,6 +70,13 @@ interface RunsTableProps { runningRecordingName: string; } +interface PaginationState { + [robotMetaId: string]: { + page: number; + rowsPerPage: number; + }; +} + export const RunsTable: React.FC = ({ currentInterpretationLog, abortRunHandler, @@ -107,27 +114,55 @@ export const RunsTable: React.FC = ({ [t] ); - const [page, setPage] = useState(0); - const [rowsPerPage, setRowsPerPage] = useState(10); + // const [page, setPage] = useState(0); + // const [rowsPerPage, setRowsPerPage] = useState(10); const [rows, setRows] = useState([]); const [searchTerm, setSearchTerm] = useState(''); const [isLoading, setIsLoading] = useState(true); + const [paginationStates, setPaginationStates] = useState({}); + const { notify, rerenderRuns, setRerenderRuns } = useGlobalInfoStore(); const handleAccordionChange = useCallback((robotMetaId: string, isExpanded: boolean) => { navigate(isExpanded ? `/runs/${robotMetaId}` : '/runs'); }, [navigate]); - const handleChangePage = useCallback((event: unknown, newPage: number) => { - setPage(newPage); + const handleChangePage = useCallback((robotMetaId: string, newPage: number) => { + setPaginationStates(prev => ({ + ...prev, + [robotMetaId]: { + ...prev[robotMetaId], + page: newPage + } + })); }, []); - const handleChangeRowsPerPage = useCallback((event: React.ChangeEvent) => { - setRowsPerPage(+event.target.value); - setPage(0); + const handleChangeRowsPerPage = useCallback((robotMetaId: string, newRowsPerPage: number) => { + setPaginationStates(prev => ({ + ...prev, + [robotMetaId]: { + page: 0, // Reset to first page when changing rows per page + rowsPerPage: newRowsPerPage + } + })); }, []); + const getPaginationState = useCallback((robotMetaId: string) => { + const defaultState = { page: 0, rowsPerPage: 10 }; + + if (!paginationStates[robotMetaId]) { + setTimeout(() => { + setPaginationStates(prev => ({ + ...prev, + [robotMetaId]: defaultState + })); + }, 0); + return defaultState; + } + return paginationStates[robotMetaId]; + }, [paginationStates]); + const debouncedSearch = useCallback((fn: Function, delay: number) => { let timeoutId: NodeJS.Timeout; return (...args: any[]) => { @@ -139,7 +174,13 @@ export const RunsTable: React.FC = ({ const handleSearchChange = useCallback((event: React.ChangeEvent) => { const debouncedSetSearch = debouncedSearch((value: string) => { setSearchTerm(value); - setPage(0); + setPaginationStates(prev => { + const reset = Object.keys(prev).reduce((acc, robotId) => ({ + ...acc, + [robotId]: { ...prev[robotId], page: 0 } + }), {}); + return reset; + }); }, 300); debouncedSetSearch(event.target.value); }, [debouncedSearch]); @@ -219,6 +260,7 @@ export const RunsTable: React.FC = ({ }; const renderTableRows = useCallback((data: Data[], robotMetaId: string) => { + const { page, rowsPerPage } = getPaginationState(robotMetaId); const start = page * rowsPerPage; const end = start + rowsPerPage; @@ -251,7 +293,7 @@ export const RunsTable: React.FC = ({ runningRecordingName={runningRecordingName} /> )); - }, [page, rowsPerPage, runId, runningRecordingName, currentInterpretationLog, abortRunHandler, handleDelete, accordionSortConfigs]); + }, [paginationStates, runId, runningRecordingName, currentInterpretationLog, abortRunHandler, handleDelete, accordionSortConfigs]); const renderSortIcon = useCallback((column: Column, robotMetaId: string) => { const sortConfig = accordionSortConfigs[robotMetaId]; @@ -369,20 +411,22 @@ export const RunsTable: React.FC = ({ {renderTableRows(data, robotMetaId)} + + handleChangePage(robotMetaId, newPage)} + onRowsPerPageChange={(event) => + handleChangeRowsPerPage(robotMetaId, +event.target.value) + } + rowsPerPageOptions={[10, 25, 50, 100]} + /> ))} - - ); }; \ No newline at end of file From b8d43216e79c71faa359bfa988c4de38e0da6518 Mon Sep 17 00:00:00 2001 From: Rohit Date: Wed, 29 Jan 2025 22:54:22 +0530 Subject: [PATCH 006/119] feat: add pagination for runs --- src/components/run/RunsTable.tsx | 179 ++++++++++++++++++------------- 1 file changed, 102 insertions(+), 77 deletions(-) diff --git a/src/components/run/RunsTable.tsx b/src/components/run/RunsTable.tsx index 04634b6d..091a6c44 100644 --- a/src/components/run/RunsTable.tsx +++ b/src/components/run/RunsTable.tsx @@ -86,6 +86,8 @@ export const RunsTable: React.FC = ({ const { t } = useTranslation(); const navigate = useNavigate(); + const [accordionPage, setAccordionPage] = useState(0); + const [accordionsPerPage, setAccordionsPerPage] = useState(10); const [accordionSortConfigs, setAccordionSortConfigs] = useState({}); const handleSort = useCallback((columnId: keyof Data, robotMetaId: string) => { @@ -114,8 +116,6 @@ export const RunsTable: React.FC = ({ [t] ); - // const [page, setPage] = useState(0); - // const [rowsPerPage, setRowsPerPage] = useState(10); const [rows, setRows] = useState([]); const [searchTerm, setSearchTerm] = useState(''); const [isLoading, setIsLoading] = useState(true); @@ -128,6 +128,15 @@ export const RunsTable: React.FC = ({ navigate(isExpanded ? `/runs/${robotMetaId}` : '/runs'); }, [navigate]); + const handleAccordionPageChange = useCallback((event: unknown, newPage: number) => { + setAccordionPage(newPage); + }, []); + + const handleAccordionsPerPageChange = useCallback((event: React.ChangeEvent) => { + setAccordionsPerPage(+event.target.value); + setAccordionPage(0); + }, []); + const handleChangePage = useCallback((robotMetaId: string, newPage: number) => { setPaginationStates(prev => ({ ...prev, @@ -174,6 +183,7 @@ export const RunsTable: React.FC = ({ const handleSearchChange = useCallback((event: React.ChangeEvent) => { const debouncedSetSearch = debouncedSearch((value: string) => { setSearchTerm(value); + setAccordionPage(0); setPaginationStates(prev => { const reset = Object.keys(prev).reduce((acc, robotId) => ({ ...acc, @@ -347,86 +357,101 @@ export const RunsTable: React.FC = ({ - {Object.entries(groupedRows).map(([robotMetaId, data]) => ( - handleAccordionChange(robotMetaId, isExpanded)} - TransitionProps={{ unmountOnExit: true }} // Optimize accordion rendering - > - }> - {data[data.length - 1].name} - - - - - - - {translatedColumns.map((column) => ( - { - if (column.id === 'startedAt' || column.id === 'finishedAt') { - handleSort(column.id, robotMetaId); - } - }} - > - - ( + handleAccordionChange(robotMetaId, isExpanded)} + TransitionProps={{ unmountOnExit: true }} // Optimize accordion rendering + > + }> + {data[data.length - 1].name} + + +
+ + + + {translatedColumns.map((column) => ( + { + if (column.id === 'startedAt' || column.id === 'finishedAt') { + handleSort(column.id, robotMetaId); } - }}> - {column.label} - + + - {renderSortIcon(column, robotMetaId)} + {column.label} + + {renderSortIcon(column, robotMetaId)} + - - - - ))} - - - - {renderTableRows(data, robotMetaId)} - -
+ + + ))} + + + + {renderTableRows(data, robotMetaId)} + + - handleChangePage(robotMetaId, newPage)} - onRowsPerPageChange={(event) => - handleChangeRowsPerPage(robotMetaId, +event.target.value) - } - rowsPerPageOptions={[10, 25, 50, 100]} - /> -
-
- ))} + handleChangePage(robotMetaId, newPage)} + onRowsPerPageChange={(event) => + handleChangeRowsPerPage(robotMetaId, +event.target.value) + } + rowsPerPageOptions={[10, 25, 50, 100]} + /> + + + ))}
+ + ); }; \ No newline at end of file From 737e0cb8fcb40cb0915305530819de6583faca8c Mon Sep 17 00:00:00 2001 From: Rohit Date: Wed, 29 Jan 2025 23:52:00 +0530 Subject: [PATCH 007/119] feat: redirect to nested run on robot run --- src/pages/MainPage.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/pages/MainPage.tsx b/src/pages/MainPage.tsx index b9a4f24f..0801a933 100644 --- a/src/pages/MainPage.tsx +++ b/src/pages/MainPage.tsx @@ -15,6 +15,7 @@ import { ScheduleSettings } from "../components/robot/ScheduleSettings"; import { IntegrationSettings } from "../components/integration/IntegrationSettings"; import { RobotSettings } from "../components/robot/RobotSettings"; import { apiUrl } from "../apiConfig"; +import { useNavigate } from 'react-router-dom'; interface MainPageProps { handleEditRecording: (id: string, fileName: string) => void; @@ -24,6 +25,7 @@ interface MainPageProps { export interface CreateRunResponse { browserId: string; runId: string; + robotMetaId: string; } export interface ScheduleRunResponse { @@ -40,12 +42,14 @@ export const MainPage = ({ handleEditRecording, initialContent }: MainPageProps) const [currentInterpretationLog, setCurrentInterpretationLog] = React.useState(''); const [ids, setIds] = React.useState({ browserId: '', - runId: '' + runId: '', + robotMetaId: '' }); let aborted = false; const { notify, setRerenderRuns, setRecordingId } = useGlobalInfoStore(); + const navigate = useNavigate(); const abortRunHandler = (runId: string) => { aborted = true; @@ -88,8 +92,9 @@ export const MainPage = ({ handleEditRecording, initialContent }: MainPageProps) }, [currentInterpretationLog]) const handleRunRecording = useCallback((settings: RunSettings) => { - createRunForStoredRecording(runningRecordingId, settings).then(({ browserId, runId }: CreateRunResponse) => { - setIds({ browserId, runId }); + createRunForStoredRecording(runningRecordingId, settings).then(({ browserId, runId, robotMetaId }: CreateRunResponse) => { + setIds({ browserId, runId, robotMetaId }); + navigate(`/runs/${robotMetaId}/run/${runId}`); const socket = io(`${apiUrl}/${browserId}`, { transports: ["websocket"], From 4f909a5fc322c1df63576951965dc492c0786276 Mon Sep 17 00:00:00 2001 From: Rohit Date: Wed, 29 Jan 2025 23:52:39 +0530 Subject: [PATCH 008/119] feat: return robot meta id --- src/api/storage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/storage.ts b/src/api/storage.ts index 290f6e7f..0bfcaf15 100644 --- a/src/api/storage.ts +++ b/src/api/storage.ts @@ -161,7 +161,7 @@ export const createRunForStoredRecording = async (id: string, settings: RunSetti } } catch (error: any) { console.log(error); - return { browserId: '', runId: '' }; + return { browserId: '', runId: '', robotMetaId: '' }; } } From ca8c51e93d72834a28e5b7301b42a4ffde2cabbb Mon Sep 17 00:00:00 2001 From: Rohit Date: Wed, 29 Jan 2025 23:53:17 +0530 Subject: [PATCH 009/119] feat: send robot meta id in response --- server/src/routes/storage.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/server/src/routes/storage.ts b/server/src/routes/storage.ts index 01f1ca6b..e159d8cb 100644 --- a/server/src/routes/storage.ts +++ b/server/src/routes/storage.ts @@ -501,6 +501,7 @@ router.put('/runs/:id', requireSignIn, async (req: AuthenticatedRequest, res) => return res.send({ browserId: id, runId: plainRun.runId, + robotMetaId: recording.recording_meta.id, }); } catch (e) { const { message } = e as Error; From eb4881a32e655bf6974f17d3e0feeab42d852ad0 Mon Sep 17 00:00:00 2001 From: Rohit Date: Wed, 29 Jan 2025 23:54:18 +0530 Subject: [PATCH 010/119] feat: open accordion based on param value --- src/components/run/RunsTable.tsx | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/components/run/RunsTable.tsx b/src/components/run/RunsTable.tsx index 92233214..6b0d479c 100644 --- a/src/components/run/RunsTable.tsx +++ b/src/components/run/RunsTable.tsx @@ -12,7 +12,7 @@ import TableRow from '@mui/material/TableRow'; import { Accordion, AccordionSummary, AccordionDetails, Typography, Box, TextField, CircularProgress, Tooltip } from '@mui/material'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import SearchIcon from '@mui/icons-material/Search'; -import { useNavigate } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router-dom'; import { useGlobalInfoStore } from "../../context/globalInfo"; import { getStoredRuns } from "../../api/storage"; import { RunSettings } from "./RunSettings"; @@ -78,6 +78,21 @@ export const RunsTable: React.FC = ({ }) => { const { t } = useTranslation(); const navigate = useNavigate(); + const location = useLocation(); + + const getUrlParams = () => { + const match = location.pathname.match(/\/runs\/([^\/]+)(?:\/run\/([^\/]+))?/); + return { + robotMetaId: match?.[1] || null, + urlRunId: match?.[2] || null + }; + }; + + const { robotMetaId: urlRobotMetaId, urlRunId } = getUrlParams(); + + const isAccordionExpanded = useCallback((currentRobotMetaId: string) => { + return currentRobotMetaId === urlRobotMetaId; + }, [urlRobotMetaId]); const [accordionSortConfigs, setAccordionSortConfigs] = useState({}); @@ -245,13 +260,14 @@ export const RunsTable: React.FC = ({ key={`row-${row.id}`} row={row} handleDelete={handleDelete} - isOpen={runId === row.runId && runningRecordingName === row.name} + isOpen={urlRunId === row.runId || (runId === row.runId && runningRecordingName === row.name)} currentLog={currentInterpretationLog} abortRunHandler={abortRunHandler} runningRecordingName={runningRecordingName} + urlRunId={urlRunId} /> )); - }, [page, rowsPerPage, runId, runningRecordingName, currentInterpretationLog, abortRunHandler, handleDelete, accordionSortConfigs]); + }, [page, rowsPerPage, runId, runningRecordingName, currentInterpretationLog, abortRunHandler, handleDelete, accordionSortConfigs, urlRunId]); const renderSortIcon = useCallback((column: Column, robotMetaId: string) => { const sortConfig = accordionSortConfigs[robotMetaId]; @@ -308,6 +324,7 @@ export const RunsTable: React.FC = ({ {Object.entries(groupedRows).map(([robotMetaId, data]) => ( handleAccordionChange(robotMetaId, isExpanded)} TransitionProps={{ unmountOnExit: true }} // Optimize accordion rendering > From b4c5622604e42ef5bdf40631f91f975cf2c8b7dd Mon Sep 17 00:00:00 2001 From: Rohit Date: Wed, 29 Jan 2025 23:55:15 +0530 Subject: [PATCH 011/119] feat: display table cell on robot meta id --- src/components/run/ColapsibleRow.tsx | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/components/run/ColapsibleRow.tsx b/src/components/run/ColapsibleRow.tsx index 8cf27d6d..87e38684 100644 --- a/src/components/run/ColapsibleRow.tsx +++ b/src/components/run/ColapsibleRow.tsx @@ -35,8 +35,9 @@ interface CollapsibleRowProps { currentLog: string; abortRunHandler: () => void; runningRecordingName: string; + urlRunId: string | null; } -export const CollapsibleRow = ({ row, handleDelete, isOpen, currentLog, abortRunHandler, runningRecordingName }: CollapsibleRowProps) => { +export const CollapsibleRow = ({ row, handleDelete, isOpen, currentLog, abortRunHandler, runningRecordingName, urlRunId }: CollapsibleRowProps) => { const { t } = useTranslation(); const navigate = useNavigate(); const [open, setOpen] = useState(isOpen); @@ -62,14 +63,18 @@ export const CollapsibleRow = ({ row, handleDelete, isOpen, currentLog, abortRun abortRunHandler(); } + useEffect(() => { + setOpen(urlRunId === row.runId || isOpen); + }, [urlRunId, row.runId, isOpen]); + const handleRowExpand = () => { const newOpen = !open; setOpen(newOpen); - if (newOpen) { - navigate(`/runs/${row.robotMetaId}/run/${row.runId}`); - } else { - navigate(`/runs/${row.robotMetaId}`); - } + navigate( + newOpen + ? `/runs/${row.robotMetaId}/run/${row.runId}` + : `/runs/${row.robotMetaId}` + ); //scrollToLogBottom(); }; From 26953a4e1eb92539af7c795d37779d01a572f573 Mon Sep 17 00:00:00 2001 From: Rohit Date: Thu, 30 Jan 2025 11:25:27 +0530 Subject: [PATCH 012/119] feat: sort robot table in desc order of creation --- src/components/robot/RecordingsTable.tsx | 38 +++++++++++++++++------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/src/components/robot/RecordingsTable.tsx b/src/components/robot/RecordingsTable.tsx index 484b1667..8b4143a2 100644 --- a/src/components/robot/RecordingsTable.tsx +++ b/src/components/robot/RecordingsTable.tsx @@ -189,23 +189,39 @@ export const RecordingsTable = ({ setPage(0); }, []); + const parseDateString = (dateStr: string): Date => { + try { + if (dateStr.includes('PM') || dateStr.includes('AM')) { + return new Date(dateStr); + } + + return new Date(dateStr.replace(/(\d+)\/(\d+)\//, '$2/$1/')) + } catch { + return new Date(0); + } + }; + const fetchRecordings = useCallback(async () => { setIsLoading(true); try { const recordings = await getStoredRecordings(); if (recordings) { const parsedRows = recordings - .map((recording: any, index: number) => { - if (recording?.recording_meta) { - return { - id: index, - ...recording.recording_meta, - content: recording.recording - }; - } - return null; - }) - .filter(Boolean); + .map((recording: any, index: number) => { + if (recording?.recording_meta) { + const parsedDate = parseDateString(recording.recording_meta.createdAt); + + return { + id: index, + ...recording.recording_meta, + content: recording.recording, + parsedDate + }; + } + return null; + }) + .filter(Boolean) + .sort((a, b) => b.parsedDate.getTime() - a.parsedDate.getTime()); setRecordings(parsedRows.map((recording) => recording.name)); setRows(parsedRows); From 752a2d12d879cc0bb5df0f80d31c13792435b0f3 Mon Sep 17 00:00:00 2001 From: Rohit Date: Thu, 30 Jan 2025 11:52:12 +0530 Subject: [PATCH 013/119] feat: sort rows based on latest run --- src/components/run/RunsTable.tsx | 41 ++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/src/components/run/RunsTable.tsx b/src/components/run/RunsTable.tsx index 92233214..563fd55c 100644 --- a/src/components/run/RunsTable.tsx +++ b/src/components/run/RunsTable.tsx @@ -194,18 +194,6 @@ export const RunsTable: React.FC = ({ return result; }, [rows, searchTerm]); - // Group filtered rows by robot meta id - const groupedRows = useMemo(() => - filteredRows.reduce((acc, row) => { - if (!acc[row.robotMetaId]) { - acc[row.robotMetaId] = []; - } - acc[row.robotMetaId].push(row); - return acc; - }, {} as Record), - [filteredRows] - ); - const parseDateString = (dateStr: string): Date => { try { if (dateStr.includes('PM') || dateStr.includes('AM')) { @@ -218,6 +206,35 @@ export const RunsTable: React.FC = ({ } }; + const groupedRows = useMemo(() => { + const groupedData = filteredRows.reduce((acc, row) => { + if (!acc[row.robotMetaId]) { + acc[row.robotMetaId] = []; + } + acc[row.robotMetaId].push(row); + return acc; + }, {} as Record); + + Object.keys(groupedData).forEach(robotId => { + groupedData[robotId].sort((a, b) => + parseDateString(b.startedAt).getTime() - parseDateString(a.startedAt).getTime() + ); + }); + + const robotEntries = Object.entries(groupedData).map(([robotId, runs]) => ({ + robotId, + runs, + latestRunDate: parseDateString(runs[0].startedAt).getTime() + })); + + robotEntries.sort((a, b) => b.latestRunDate - a.latestRunDate); + + return robotEntries.reduce((acc, { robotId, runs }) => { + acc[robotId] = runs; + return acc; + }, {} as Record); + }, [filteredRows]); + const renderTableRows = useCallback((data: Data[], robotMetaId: string) => { const start = page * rowsPerPage; const end = start + rowsPerPage; From 3bc2376f153af2333ee3c2849e736c2984622783 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Thu, 30 Jan 2025 18:20:27 +0530 Subject: [PATCH 014/119] feat: remove star counter --- src/components/dashboard/NavBar.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/components/dashboard/NavBar.tsx b/src/components/dashboard/NavBar.tsx index ec5fcf7c..d312f817 100644 --- a/src/components/dashboard/NavBar.tsx +++ b/src/components/dashboard/NavBar.tsx @@ -318,7 +318,14 @@ export const NavBar: React.FC = ({ )} - + {/* */} Date: Thu, 30 Jan 2025 19:54:19 +0530 Subject: [PATCH 015/119] feat: gh icon --- src/components/dashboard/NavBar.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/dashboard/NavBar.tsx b/src/components/dashboard/NavBar.tsx index d312f817..738ebef9 100644 --- a/src/components/dashboard/NavBar.tsx +++ b/src/components/dashboard/NavBar.tsx @@ -24,6 +24,7 @@ import { Clear, YouTube, X, + GitHub, Update, Close, Language, From 752d227b21c5876b02863a04f05bda71ea368117 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Thu, 30 Jan 2025 19:56:20 +0530 Subject: [PATCH 016/119] feat: link github repo --- src/components/dashboard/NavBar.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/dashboard/NavBar.tsx b/src/components/dashboard/NavBar.tsx index 738ebef9..6b1e3572 100644 --- a/src/components/dashboard/NavBar.tsx +++ b/src/components/dashboard/NavBar.tsx @@ -359,6 +359,11 @@ export const NavBar: React.FC = ({ }}> Docs + { + window.open('https://github.com/getmaxun/maxun', '_blank'); + }}> + GitHub + { window.open('https://discord.gg/5GbPjBUkws', '_blank'); }}> From e6576de3c8ac06238e5b7b1634bde426d53c6020 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Thu, 30 Jan 2025 19:57:06 +0530 Subject: [PATCH 017/119] feat: use github icon --- src/components/dashboard/NavBar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/dashboard/NavBar.tsx b/src/components/dashboard/NavBar.tsx index 6b1e3572..4c13ed96 100644 --- a/src/components/dashboard/NavBar.tsx +++ b/src/components/dashboard/NavBar.tsx @@ -362,7 +362,7 @@ export const NavBar: React.FC = ({ { window.open('https://github.com/getmaxun/maxun', '_blank'); }}> - GitHub + GitHub { window.open('https://discord.gg/5GbPjBUkws', '_blank'); From 1854194a93e2d115fdce46d4a677f42d05c0c3f8 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Thu, 30 Jan 2025 19:59:35 +0530 Subject: [PATCH 018/119] feat: move language above in the menu --- src/components/dashboard/NavBar.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/dashboard/NavBar.tsx b/src/components/dashboard/NavBar.tsx index 4c13ed96..59519737 100644 --- a/src/components/dashboard/NavBar.tsx +++ b/src/components/dashboard/NavBar.tsx @@ -354,6 +354,9 @@ export const NavBar: React.FC = ({ { handleMenuClose(); logout(); }}> {t('navbar.menu_items.logout')} + + {t('navbar.menu_items.language')} + { window.open('https://docs.maxun.dev', '_blank'); }}> @@ -379,9 +382,6 @@ export const NavBar: React.FC = ({ }}> Twitter (X) - - {t('navbar.menu_items.language')} - Date: Thu, 30 Jan 2025 20:00:32 +0530 Subject: [PATCH 019/119] feat: add horizontal row --- src/components/dashboard/NavBar.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/dashboard/NavBar.tsx b/src/components/dashboard/NavBar.tsx index 59519737..8f1590ec 100644 --- a/src/components/dashboard/NavBar.tsx +++ b/src/components/dashboard/NavBar.tsx @@ -357,6 +357,7 @@ export const NavBar: React.FC = ({ {t('navbar.menu_items.language')} +
{ window.open('https://docs.maxun.dev', '_blank'); }}> From 40fadedd95a42aae8a2507a0479af3d184d831e7 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Thu, 30 Jan 2025 21:51:31 +0530 Subject: [PATCH 020/119] fix: alignment of nav items --- src/components/dashboard/NavBar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/dashboard/NavBar.tsx b/src/components/dashboard/NavBar.tsx index d312f817..c007b358 100644 --- a/src/components/dashboard/NavBar.tsx +++ b/src/components/dashboard/NavBar.tsx @@ -220,7 +220,7 @@ export const NavBar: React.FC = ({ {!isRecording ? ( <> From 6e93fec28e9e813c78010a9d83bbe63ee6c5efa8 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Wed, 5 Feb 2025 22:35:19 +0530 Subject: [PATCH 065/119] feat: use icon button --- src/components/dashboard/NavBar.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/dashboard/NavBar.tsx b/src/components/dashboard/NavBar.tsx index 14bbb225..5b6e8338 100644 --- a/src/components/dashboard/NavBar.tsx +++ b/src/components/dashboard/NavBar.tsx @@ -220,13 +220,13 @@ export const NavBar: React.FC = ({
{!isRecording ? ( <> - + Date: Wed, 5 Feb 2025 22:36:30 +0530 Subject: [PATCH 066/119] feat: icon button styles --- src/components/dashboard/NavBar.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/dashboard/NavBar.tsx b/src/components/dashboard/NavBar.tsx index 5b6e8338..1a64f71a 100644 --- a/src/components/dashboard/NavBar.tsx +++ b/src/components/dashboard/NavBar.tsx @@ -221,9 +221,11 @@ export const NavBar: React.FC = ({ {!isRecording ? ( <> {t('navbar.upgrade.button')} From 1a5beae97cfe9cf6ce3e939d13a94d40b9219a6a Mon Sep 17 00:00:00 2001 From: amhsirak Date: Wed, 5 Feb 2025 22:38:03 +0530 Subject: [PATCH 067/119] feat: wrap text inside typography body 1 --- src/components/dashboard/NavBar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/dashboard/NavBar.tsx b/src/components/dashboard/NavBar.tsx index 1a64f71a..6bd57a77 100644 --- a/src/components/dashboard/NavBar.tsx +++ b/src/components/dashboard/NavBar.tsx @@ -227,7 +227,7 @@ export const NavBar: React.FC = ({ padding: '8px', marginRight: '10px', }}> - {t('navbar.upgrade.button')} + {t('navbar.upgrade.button')} Date: Wed, 5 Feb 2025 22:38:29 +0530 Subject: [PATCH 068/119] feat: move typographu --- src/components/dashboard/NavBar.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/dashboard/NavBar.tsx b/src/components/dashboard/NavBar.tsx index 6bd57a77..a41c6a42 100644 --- a/src/components/dashboard/NavBar.tsx +++ b/src/components/dashboard/NavBar.tsx @@ -227,7 +227,8 @@ export const NavBar: React.FC = ({ padding: '8px', marginRight: '10px', }}> - {t('navbar.upgrade.button')} + + {t('navbar.upgrade.button')} Date: Wed, 5 Feb 2025 22:38:41 +0530 Subject: [PATCH 069/119] chore: lint --- src/components/dashboard/NavBar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/dashboard/NavBar.tsx b/src/components/dashboard/NavBar.tsx index a41c6a42..c35fc60d 100644 --- a/src/components/dashboard/NavBar.tsx +++ b/src/components/dashboard/NavBar.tsx @@ -227,7 +227,7 @@ export const NavBar: React.FC = ({ padding: '8px', marginRight: '10px', }}> - + {t('navbar.upgrade.button')} From 5ea92572d8124c7f9954e81b0fe207bfe85e520d Mon Sep 17 00:00:00 2001 From: amhsirak Date: Wed, 5 Feb 2025 22:39:51 +0530 Subject: [PATCH 070/119] feat: increase spacing --- src/components/dashboard/NavBar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/dashboard/NavBar.tsx b/src/components/dashboard/NavBar.tsx index c35fc60d..8a9b1e9c 100644 --- a/src/components/dashboard/NavBar.tsx +++ b/src/components/dashboard/NavBar.tsx @@ -225,7 +225,7 @@ export const NavBar: React.FC = ({ alignItems: 'center', borderRadius: '5px', padding: '8px', - marginRight: '10px', + marginRight: '20px', }}> {t('navbar.upgrade.button')} From f63ff72d4a8a1f393eab69cac86765d50bc564ae Mon Sep 17 00:00:00 2001 From: Rohit Date: Fri, 7 Feb 2025 17:32:00 +0530 Subject: [PATCH 071/119] feat: add selection start property to get cursor index --- .../workflow-management/classes/Generator.ts | 62 ++++++++++++------- 1 file changed, 40 insertions(+), 22 deletions(-) diff --git a/server/src/workflow-management/classes/Generator.ts b/server/src/workflow-management/classes/Generator.ts index 5980628b..38237312 100644 --- a/server/src/workflow-management/classes/Generator.ts +++ b/server/src/workflow-management/classes/Generator.ts @@ -435,32 +435,33 @@ export class WorkflowGenerator { if (!element) return null; const getCursorPosition = (inputElement: HTMLInputElement | HTMLTextAreaElement, clickCoords: Coordinates) => { - const rect = inputElement.getBoundingClientRect(); - const clickX = clickCoords.x - rect.left; + // const rect = inputElement.getBoundingClientRect(); + // const clickX = clickCoords.x - rect.left; - // Get the input's text content - const text = inputElement.value; + // // Get the input's text content + // const text = inputElement.value; - // Create a temporary element to measure text - const measurer = document.createElement('span'); - measurer.style.font = window.getComputedStyle(inputElement).font; - measurer.style.position = 'absolute'; - measurer.style.whiteSpace = 'pre'; - measurer.style.visibility = 'hidden'; - document.body.appendChild(measurer); + // // Create a temporary element to measure text + // const measurer = document.createElement('span'); + // measurer.style.font = window.getComputedStyle(inputElement).font; + // measurer.style.position = 'absolute'; + // measurer.style.whiteSpace = 'pre'; + // measurer.style.visibility = 'hidden'; + // document.body.appendChild(measurer); - // Find the position where the click occurred - let position = 0; - for (let i = 0; i <= text.length; i++) { - measurer.textContent = text.slice(0, i); - const width = measurer.getBoundingClientRect().width; - if (width >= clickX) { - position = i; - break; - } - } + // // Find the position where the click occurred + // let position = 0; + // for (let i = 0; i <= text.length; i++) { + // measurer.textContent = text.slice(0, i); + // const width = measurer.getBoundingClientRect().width; + // if (width >= clickX) { + // position = i; + // break; + // } + // } - document.body.removeChild(measurer); + // document.body.removeChild(measurer); + const position = inputElement.selectionStart || 0; return position; }; @@ -740,6 +741,23 @@ export class WorkflowGenerator { * @returns {Promise} */ public saveNewWorkflow = async (fileName: string, userId: number, isLogin: boolean) => { + for (const pair of this.workflowRecord.workflow) { + for (let i = 0; i < pair.what.length; i++) { + const condition = pair.what[i]; + + if (condition.action === 'press' && condition.args) { + const [selector, encryptedKey, type] = condition.args; + const key = decrypt(encryptedKey); + + console.log(`Selector: ${selector}, Key: ${key}`); + } + + if (condition.action === 'click' && condition.args) { + console.log("Click args: ", condition.args); + } + } + } + const recording = this.optimizeWorkflow(this.workflowRecord); try { this.recordingMeta = { From 2199149827625be50a20c63428e37dc26db26fcc Mon Sep 17 00:00:00 2001 From: Rohit Date: Fri, 7 Feb 2025 17:33:12 +0530 Subject: [PATCH 072/119] feat: rm previous cursor logic --- .../workflow-management/classes/Generator.ts | 26 ------------------- 1 file changed, 26 deletions(-) diff --git a/server/src/workflow-management/classes/Generator.ts b/server/src/workflow-management/classes/Generator.ts index 38237312..ff36bed9 100644 --- a/server/src/workflow-management/classes/Generator.ts +++ b/server/src/workflow-management/classes/Generator.ts @@ -435,32 +435,6 @@ export class WorkflowGenerator { if (!element) return null; const getCursorPosition = (inputElement: HTMLInputElement | HTMLTextAreaElement, clickCoords: Coordinates) => { - // const rect = inputElement.getBoundingClientRect(); - // const clickX = clickCoords.x - rect.left; - - // // Get the input's text content - // const text = inputElement.value; - - // // Create a temporary element to measure text - // const measurer = document.createElement('span'); - // measurer.style.font = window.getComputedStyle(inputElement).font; - // measurer.style.position = 'absolute'; - // measurer.style.whiteSpace = 'pre'; - // measurer.style.visibility = 'hidden'; - // document.body.appendChild(measurer); - - // // Find the position where the click occurred - // let position = 0; - // for (let i = 0; i <= text.length; i++) { - // measurer.textContent = text.slice(0, i); - // const width = measurer.getBoundingClientRect().width; - // if (width >= clickX) { - // position = i; - // break; - // } - // } - - // document.body.removeChild(measurer); const position = inputElement.selectionStart || 0; return position; }; From 0abb78648009aff14d76134b6e90e712f29ea24f Mon Sep 17 00:00:00 2001 From: amhsirak Date: Sat, 8 Feb 2025 15:38:39 +0530 Subject: [PATCH 073/119] feat: show icon as per key visibility --- src/components/api/ApiKey.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/api/ApiKey.tsx b/src/components/api/ApiKey.tsx index 9d54fe5c..3a085576 100644 --- a/src/components/api/ApiKey.tsx +++ b/src/components/api/ApiKey.tsx @@ -137,7 +137,7 @@ const ApiKeyManager = () => { setShowKey(!showKey)}> - + {showKey ? : } From ebcd117694b03b76fdff2cd681efd84e40cda42d Mon Sep 17 00:00:00 2001 From: amhsirak Date: Sat, 8 Feb 2025 15:44:36 +0530 Subject: [PATCH 074/119] feat: use visibility off icon --- src/components/api/ApiKey.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/api/ApiKey.tsx b/src/components/api/ApiKey.tsx index 3a085576..9a1a492a 100644 --- a/src/components/api/ApiKey.tsx +++ b/src/components/api/ApiKey.tsx @@ -14,7 +14,7 @@ import { Tooltip, Paper, } from '@mui/material'; -import { ContentCopy, Visibility, Delete } from '@mui/icons-material'; +import { ContentCopy, Visibility, VisibilityOff, Delete } from '@mui/icons-material'; import styled from 'styled-components'; import axios from 'axios'; import { useGlobalInfoStore } from '../../context/globalInfo'; @@ -137,7 +137,7 @@ const ApiKeyManager = () => { setShowKey(!showKey)}> - {showKey ? : } + {showKey ? : } From 32b25837e4ce25e31224fb1a2bd923381d5e9da7 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Sat, 8 Feb 2025 15:51:45 +0530 Subject: [PATCH 075/119] fix: use correct icons based on showKey state --- src/components/api/ApiKey.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/api/ApiKey.tsx b/src/components/api/ApiKey.tsx index 9a1a492a..f84f7a55 100644 --- a/src/components/api/ApiKey.tsx +++ b/src/components/api/ApiKey.tsx @@ -137,7 +137,7 @@ const ApiKeyManager = () => { setShowKey(!showKey)}> - {showKey ? : } + {showKey ? : } From fcb75f412fafd767f3b07872d84c4233b3ea9d12 Mon Sep 17 00:00:00 2001 From: Rohit Date: Mon, 10 Feb 2025 14:28:50 +0530 Subject: [PATCH 076/119] feat: notify based on error status --- src/components/api/ApiKey.tsx | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/components/api/ApiKey.tsx b/src/components/api/ApiKey.tsx index 9d54fe5c..e92bd368 100644 --- a/src/components/api/ApiKey.tsx +++ b/src/components/api/ApiKey.tsx @@ -43,8 +43,33 @@ const ApiKeyManager = () => { try { const { data } = await axios.get(`${apiUrl}/auth/api-key`); setApiKey(data.api_key); + notify('success', t('apikey.notifications.success.fetch')); } catch (error: any) { - notify('error', t('apikey.notifications.fetch_error', { error: error.message })); + const status = error.response?.status; + let errorKey = 'unknown'; + + switch (status) { + case 401: + errorKey = 'unauthorized'; + break; + case 404: + errorKey = 'not_found'; + break; + case 500: + errorKey = 'server'; + break; + default: + if (error.message?.includes('Network Error')) { + errorKey = 'network'; + } + } + + notify( + 'error', + t(`apikey.notifications.errors.fetch.${errorKey}`, { + error: error.response?.data?.message || error.message + }) + ); } finally { setLoading(false); } From 744778a4cecdfd77977bc0397aa7575a9b0eeb1d Mon Sep 17 00:00:00 2001 From: Rohit Date: Mon, 10 Feb 2025 14:37:58 +0530 Subject: [PATCH 077/119] feat: return code for better error handling --- server/src/routes/auth.ts | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts index b7d884a4..39efbede 100644 --- a/server/src/routes/auth.ts +++ b/server/src/routes/auth.ts @@ -261,7 +261,11 @@ router.get( const authenticatedReq = req as AuthenticatedRequest; try { if (!authenticatedReq.user) { - return res.status(401).json({ ok: false, error: "Unauthorized" }); + return res.status(401).json({ + ok: false, + error: "Unauthorized", + code: "unauthorized" + }); } const user = await User.findByPk(authenticatedReq.user.id, { @@ -270,15 +274,25 @@ router.get( }); if (!user) { - return res.status(404).json({ message: "User not found" }); + return res.status(404).json({ + ok: false, + error: "User not found", + code: "not_found" + }); } return res.status(200).json({ + ok: true, message: "API key fetched successfully", api_key: user.api_key || null, }); } catch (error) { - return res.status(500).json({ message: "Error fetching API key", error }); + console.error('API Key fetch error:', error); + return res.status(500).json({ + ok: false, + error: "Error fetching API key", + code: "server", + }); } } ); From 85132c3d509e25d2b7a619432f6fe84ec0825a4a Mon Sep 17 00:00:00 2001 From: Rohit Date: Mon, 10 Feb 2025 15:05:49 +0530 Subject: [PATCH 078/119] feat: add translation for api key notifications --- public/locales/de.json | 41 +++++++++++++++++++++++++++++++++++------ public/locales/en.json | 41 +++++++++++++++++++++++++++++++++++------ public/locales/es.json | 41 +++++++++++++++++++++++++++++++++++------ public/locales/ja.json | 41 +++++++++++++++++++++++++++++++++++------ public/locales/zh.json | 41 +++++++++++++++++++++++++++++++++++------ 5 files changed, 175 insertions(+), 30 deletions(-) diff --git a/public/locales/de.json b/public/locales/de.json index a4c02b1d..aa25f0ce 100644 --- a/public/locales/de.json +++ b/public/locales/de.json @@ -139,12 +139,41 @@ "no_key_message": "Sie haben noch keinen API-Schlüssel generiert.", "generate_button": "API-Schlüssel generieren", "notifications": { - "fetch_error": "API-Schlüssel konnte nicht abgerufen werden - ${error}", - "generate_success": "API-Schlüssel erfolgreich generiert", - "generate_error": "API-Schlüssel konnte nicht generiert werden - ${error}", - "delete_success": "API-Schlüssel erfolgreich gelöscht", - "delete_error": "API-Schlüssel konnte nicht gelöscht werden - ${error}", - "copy_success": "API-Schlüssel erfolgreich kopiert" + "errors": { + "fetch": { + "network": "Netzwerkfehler beim Abrufen des API-Schlüssels: ${error}", + "unauthorized": "Sie müssen angemeldet sein, um auf den API-Schlüssel zuzugreifen", + "not_found": "API-Schlüssel für Ihr Konto wurde nicht gefunden", + "server": "Serverfehler beim Abrufen des API-Schlüssels. Bitte versuchen Sie es später erneut", + "unknown": "Unbekannter Fehler beim Abrufen des API-Schlüssels: ${error}" + }, + "generate": { + "network": "Netzwerkfehler bei der Generierung des API-Schlüssels: ${error}", + "unauthorized": "Sie müssen angemeldet sein, um einen API-Schlüssel zu generieren", + "key_exists": "Sie haben bereits einen API-Schlüssel. Bitte löschen Sie zuerst den vorhandenen", + "not_found": "Benutzerkonto nicht gefunden", + "server": "Serverfehler bei der Generierung des API-Schlüssels. Bitte versuchen Sie es später erneut", + "unknown": "Unbekannter Fehler bei der Generierung des API-Schlüssels: ${error}" + }, + "delete": { + "network": "Netzwerkfehler beim Löschen des API-Schlüssels: ${error}", + "unauthorized": "Sie müssen angemeldet sein, um den API-Schlüssel zu löschen", + "not_found": "Benutzerkonto nicht gefunden", + "key_not_found": "Kein API-Schlüssel zum Löschen gefunden", + "server": "Serverfehler beim Löschen des API-Schlüssels. Bitte versuchen Sie es später erneut", + "unknown": "Unbekannter Fehler beim Löschen des API-Schlüssels: ${error}" + }, + "copy": { + "failed": "Fehler beim Kopieren des API-Schlüssels in die Zwischenablage", + "no_key": "Kein API-Schlüssel zum Kopieren verfügbar" + } + }, + "success": { + "fetch": "API-Schlüssel erfolgreich abgerufen", + "generate": "Neuer API-Schlüssel erfolgreich generiert", + "delete": "API-Schlüssel erfolgreich gelöscht", + "copy": "API-Schlüssel in die Zwischenablage kopiert" + } } }, "action_description": { diff --git a/public/locales/en.json b/public/locales/en.json index f072dcc9..0cf1cf35 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -140,12 +140,41 @@ "no_key_message": "You haven't generated an API key yet.", "generate_button": "Generate API Key", "notifications": { - "fetch_error": "Failed to fetch API Key - ${error}", - "generate_success": "Generated API Key successfully", - "generate_error": "Failed to generate API Key - ${error}", - "delete_success": "API Key deleted successfully", - "delete_error": "Failed to delete API Key - ${error}", - "copy_success": "Copied API Key successfully" + "errors": { + "fetch": { + "network": "Network error while fetching API key: ${error}", + "unauthorized": "You must be logged in to access API key", + "not_found": "Unable to find API key for your account", + "server": "Server error while fetching API key. Please try again later", + "unknown": "Unknown error occurred while fetching API key: ${error}" + }, + "generate": { + "network": "Network error while generating API key: ${error}", + "unauthorized": "You must be logged in to generate an API key", + "key_exists": "You already have an API key. Please delete the existing one first", + "not_found": "User account not found", + "server": "Server error while generating API key. Please try again later", + "unknown": "Unknown error occurred while generating API key: ${error}" + }, + "delete": { + "network": "Network error while deleting API key: ${error}", + "unauthorized": "You must be logged in to delete API key", + "not_found": "User account not found", + "key_not_found": "No API key found to delete", + "server": "Server error while deleting API key. Please try again later", + "unknown": "Unknown error occurred while deleting API key: ${error}" + }, + "copy": { + "failed": "Failed to copy API key to clipboard", + "no_key": "No API key available to copy" + } + }, + "success": { + "fetch": "API key retrieved successfully", + "generate": "New API key generated successfully", + "delete": "API key deleted successfully", + "copy": "API key copied to clipboard" + } } }, "action_description": { diff --git a/public/locales/es.json b/public/locales/es.json index 09f6d703..fb0c4e54 100644 --- a/public/locales/es.json +++ b/public/locales/es.json @@ -140,12 +140,41 @@ "no_key_message": "Aún no has generado una clave API.", "generate_button": "Generar Clave API", "notifications": { - "fetch_error": "Error al obtener la clave API - ${error}", - "generate_success": "Clave API generada con éxito", - "generate_error": "Error al generar la clave API - ${error}", - "delete_success": "Clave API eliminada con éxito", - "delete_error": "Error al eliminar la clave API - ${error}", - "copy_success": "Clave API copiada con éxito" + "errors": { + "fetch": { + "network": "Error de red al obtener la clave API: ${error}", + "unauthorized": "Debes iniciar sesión para acceder a la clave API", + "not_found": "No se pudo encontrar la clave API para tu cuenta", + "server": "Error del servidor al obtener la clave API. Por favor, inténtalo más tarde", + "unknown": "Error desconocido al obtener la clave API: ${error}" + }, + "generate": { + "network": "Error de red al generar la clave API: ${error}", + "unauthorized": "Debes iniciar sesión para generar una clave API", + "key_exists": "Ya tienes una clave API. Por favor, elimina la existente primero", + "not_found": "Cuenta de usuario no encontrada", + "server": "Error del servidor al generar la clave API. Por favor, inténtalo más tarde", + "unknown": "Error desconocido al generar la clave API: ${error}" + }, + "delete": { + "network": "Error de red al eliminar la clave API: ${error}", + "unauthorized": "Debes iniciar sesión para eliminar la clave API", + "not_found": "Cuenta de usuario no encontrada", + "key_not_found": "No se encontró ninguna clave API para eliminar", + "server": "Error del servidor al eliminar la clave API. Por favor, inténtalo más tarde", + "unknown": "Error desconocido al eliminar la clave API: ${error}" + }, + "copy": { + "failed": "Error al copiar la clave API al portapapeles", + "no_key": "No hay clave API disponible para copiar" + } + }, + "success": { + "fetch": "Clave API obtenida exitosamente", + "generate": "Nueva clave API generada exitosamente", + "delete": "Clave API eliminada exitosamente", + "copy": "Clave API copiada al portapapeles" + } } }, "action_description": { diff --git a/public/locales/ja.json b/public/locales/ja.json index d2fe42ea..9f852836 100644 --- a/public/locales/ja.json +++ b/public/locales/ja.json @@ -140,12 +140,41 @@ "no_key_message": "APIキーはまだ生成されていません。", "generate_button": "APIキーを生成", "notifications": { - "fetch_error": "APIキーの取得に失敗しました - ${error}", - "generate_success": "APIキーが正常に生成されました", - "generate_error": "APIキーの生成に失敗しました - ${error}", - "delete_success": "APIキーが正常に削除されました", - "delete_error": "APIキーの削除に失敗しました - ${error}", - "copy_success": "APIキーがコピーされました" + "errors": { + "fetch": { + "network": "APIキーの取得中にネットワークエラーが発生しました:${error}", + "unauthorized": "APIキーにアクセスするにはログインが必要です", + "not_found": "アカウントのAPIキーが見つかりません", + "server": "APIキーの取得中にサーバーエラーが発生しました。後でもう一度お試しください", + "unknown": "APIキーの取得中に不明なエラーが発生しました:${error}" + }, + "generate": { + "network": "APIキーの生成中にネットワークエラーが発生しました:${error}", + "unauthorized": "APIキーを生成するにはログインが必要です", + "key_exists": "APIキーが既に存在します。既存のキーを先に削除してください", + "not_found": "ユーザーアカウントが見つかりません", + "server": "APIキーの生成中にサーバーエラーが発生しました。後でもう一度お試しください", + "unknown": "APIキーの生成中に不明なエラーが発生しました:${error}" + }, + "delete": { + "network": "APIキーの削除中にネットワークエラーが発生しました:${error}", + "unauthorized": "APIキーを削除するにはログインが必要です", + "not_found": "ユーザーアカウントが見つかりません", + "key_not_found": "削除するAPIキーが見つかりません", + "server": "APIキーの削除中にサーバーエラーが発生しました。後でもう一度お試しください", + "unknown": "APIキーの削除中に不明なエラーが発生しました:${error}" + }, + "copy": { + "failed": "APIキーのクリップボードへのコピーに失敗しました", + "no_key": "コピーできるAPIキーがありません" + } + }, + "success": { + "fetch": "APIキーの取得に成功しました", + "generate": "新しいAPIキーの生成に成功しました", + "delete": "APIキーの削除に成功しました", + "copy": "APIキーをクリップボードにコピーしました" + } } }, "action_description": { diff --git a/public/locales/zh.json b/public/locales/zh.json index e7c58660..80fec31e 100644 --- a/public/locales/zh.json +++ b/public/locales/zh.json @@ -140,12 +140,41 @@ "no_key_message": "您还未生成API密钥。", "generate_button": "生成API密钥", "notifications": { - "fetch_error": "获取API密钥失败 - ${error}", - "generate_success": "API密钥生成成功", - "generate_error": "生成API密钥失败 - ${error}", - "delete_success": "API密钥删除成功", - "delete_error": "删除API密钥失败 - ${error}", - "copy_success": "API密钥复制成功" + "errors": { + "fetch": { + "network": "获取API密钥时发生网络错误:${error}", + "unauthorized": "您必须登录才能访问API密钥", + "not_found": "找不到您账户的API密钥", + "server": "获取API密钥时发生服务器错误。请稍后重试", + "unknown": "获取API密钥时发生未知错误:${error}" + }, + "generate": { + "network": "生成API密钥时发生网络错误:${error}", + "unauthorized": "您必须登录才能生成API密钥", + "key_exists": "您已经有一个API密钥。请先删除现有的密钥", + "not_found": "找不到用户账户", + "server": "生成API密钥时发生服务器错误。请稍后重试", + "unknown": "生成API密钥时发生未知错误:${error}" + }, + "delete": { + "network": "删除API密钥时发生网络错误:${error}", + "unauthorized": "您必须登录才能删除API密钥", + "not_found": "找不到用户账户", + "key_not_found": "找不到要删除的API密钥", + "server": "删除API密钥时发生服务器错误。请稍后重试", + "unknown": "删除API密钥时发生未知错误:${error}" + }, + "copy": { + "failed": "复制API密钥到剪贴板失败", + "no_key": "没有可复制的API密钥" + } + }, + "success": { + "fetch": "成功获取API密钥", + "generate": "成功生成新的API密钥", + "delete": "成功删除API密钥", + "copy": "已将API密钥复制到剪贴板" + } } }, "action_description": { From ed5e96b476c8787aecdc1565c95d9bc2045dc7e9 Mon Sep 17 00:00:00 2001 From: Rohit Date: Mon, 10 Feb 2025 15:06:21 +0530 Subject: [PATCH 079/119] feat: better notifications for api key page --- src/components/api/ApiKey.tsx | 81 +++++++++++++++++++++++++++++------ 1 file changed, 69 insertions(+), 12 deletions(-) diff --git a/src/components/api/ApiKey.tsx b/src/components/api/ApiKey.tsx index e92bd368..5afc6da1 100644 --- a/src/components/api/ApiKey.tsx +++ b/src/components/api/ApiKey.tsx @@ -83,11 +83,36 @@ const ApiKeyManager = () => { setLoading(true); try { const { data } = await axios.post(`${apiUrl}/auth/generate-api-key`); - setApiKey(data.api_key); - - notify('success', t('apikey.notifications.generate_success')); + if (data.ok && data.api_key) { + setApiKey(data.api_key); + notify('success', t('apikey.notifications.success.generate')); + } } catch (error: any) { - notify('error', t('apikey.notifications.generate_error', { error: error.message })); + const status = error.response?.status; + let errorKey = 'unknown'; + + switch (status) { + case 401: + errorKey = 'unauthorized'; + break; + case 403: + errorKey = 'limit_reached'; + break; + case 500: + errorKey = 'server'; + break; + default: + if (error.message?.includes('Network Error')) { + errorKey = 'network'; + } + } + + notify( + 'error', + t(`apikey.notifications.errors.generate.${errorKey}`, { + error: error.response?.data?.message || error.message + }) + ); } finally { setLoading(false); } @@ -96,22 +121,54 @@ const ApiKeyManager = () => { const deleteApiKey = async () => { setLoading(true); try { - await axios.delete(`${apiUrl}/auth/delete-api-key`); - setApiKey(null); - notify('success', t('apikey.notifications.delete_success')); + const response = await axios.delete(`${apiUrl}/auth/delete-api-key`); + if (response.data.ok) { + setApiKey(null); + notify('success', t('apikey.notifications.success.delete')); + } } catch (error: any) { - notify('error', t('apikey.notifications.delete_error', { error: error.message })); + const status = error.response?.status; + let errorKey = 'unknown'; + + switch (status) { + case 401: + errorKey = 'unauthorized'; + break; + case 404: + errorKey = 'not_found'; + break; + case 500: + errorKey = 'server'; + break; + default: + if (error.message?.includes('Network Error')) { + errorKey = 'network'; + } + } + + notify( + 'error', + t(`apikey.notifications.errors.delete.${errorKey}`, { + error: error.response?.data?.message || error.message + }) + ); } finally { setLoading(false); } }; - const copyToClipboard = () => { - if (apiKey) { - navigator.clipboard.writeText(apiKey); + const copyToClipboard = async () => { + if (!apiKey) return; + + try { + await navigator.clipboard.writeText(apiKey); setCopySuccess(true); + notify('success', t('apikey.notifications.success.copy')); + + // Reset copy success state after 2 seconds setTimeout(() => setCopySuccess(false), 2000); - notify('info', t('apikey.notifications.copy_success')); + } catch (error) { + notify('error', t('apikey.notifications.errors.copy.failed')); } }; From d800158f9980f8369c40a2d328d16f78d1e51a26 Mon Sep 17 00:00:00 2001 From: Rohit Date: Mon, 10 Feb 2025 15:06:47 +0530 Subject: [PATCH 080/119] feat: add error codes for api key routes --- server/src/routes/auth.ts | 98 +++++++++++++++++++++++++++++---------- 1 file changed, 73 insertions(+), 25 deletions(-) diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts index 39efbede..0980b400 100644 --- a/server/src/routes/auth.ts +++ b/server/src/routes/auth.ts @@ -220,36 +220,56 @@ router.post( const authenticatedReq = req as AuthenticatedRequest; try { if (!authenticatedReq.user) { - return res.status(401).json({ ok: false, error: "Unauthorized" }); + return res.status(401).json({ + ok: false, + message: "Unauthorized", + code: "unauthorized" + }); } + const user = await User.findByPk(authenticatedReq.user.id, { attributes: { exclude: ["password"] }, }); if (!user) { - return res.status(404).json({ message: "User not found" }); + return res.status(404).json({ + ok: false, + message: "User not found", + code: "not_found" + }); } if (user.api_key) { - return res.status(400).json({ message: "API key already exists" }); + return res.status(400).json({ + ok: false, + message: "API key already exists", + code: "key_exists" + }); } - const apiKey = genAPIKey(); + const apiKey = genAPIKey(); await user.update({ api_key: apiKey }); + // Capture analytics event capture("maxun-oss-api-key-created", { user_id: user.id, created_at: new Date().toISOString(), }); return res.status(200).json({ + ok: true, message: "API key generated successfully", - api_key: apiKey, + api_key: apiKey }); + } catch (error) { - return res - .status(500) - .json({ message: "Error generating API key", error }); + console.error('API Key generation error:', error); + return res.status(500).json({ + ok: false, + message: "Error generating API key", + code: "server", + error: process.env.NODE_ENV === 'development' ? error : undefined + }); } } ); @@ -263,7 +283,7 @@ router.get( if (!authenticatedReq.user) { return res.status(401).json({ ok: false, - error: "Unauthorized", + message: "Unauthorized", code: "unauthorized" }); } @@ -276,7 +296,7 @@ router.get( if (!user) { return res.status(404).json({ ok: false, - error: "User not found", + message: "User not found", code: "not_found" }); } @@ -284,14 +304,16 @@ router.get( return res.status(200).json({ ok: true, message: "API key fetched successfully", - api_key: user.api_key || null, + api_key: user.api_key || null }); + } catch (error) { console.error('API Key fetch error:', error); return res.status(500).json({ ok: false, - error: "Error fetching API key", + message: "Error fetching API key", code: "server", + error: process.env.NODE_ENV === 'development' ? error : undefined }); } } @@ -302,33 +324,59 @@ router.delete( requireSignIn, async (req: Request, res) => { const authenticatedReq = req as AuthenticatedRequest; - if (!authenticatedReq.user) { - return res.status(401).send({ error: "Unauthorized" }); - } - try { - const user = await User.findByPk(authenticatedReq.user.id, { raw: true }); + if (!authenticatedReq.user) { + return res.status(401).json({ + ok: false, + message: "Unauthorized", + code: "unauthorized" + }); + } + + const user = await User.findByPk(authenticatedReq.user.id, { + raw: true, + attributes: ["id", "api_key"] + }); if (!user) { - return res.status(404).json({ message: "User not found" }); + return res.status(404).json({ + ok: false, + message: "User not found", + code: "not_found" + }); } if (!user.api_key) { - return res.status(404).json({ message: "API Key not found" }); + return res.status(404).json({ + ok: false, + message: "API Key not found", + code: "key_not_found" + }); } - await User.update({ api_key: null }, { where: { id: authenticatedReq.user.id } }); + await User.update( + { api_key: null }, + { where: { id: authenticatedReq.user.id } } + ); capture("maxun-oss-api-key-deleted", { user_id: user.id, deleted_at: new Date().toISOString(), }); - return res.status(200).json({ message: "API Key deleted successfully" }); - } catch (error: any) { - return res - .status(500) - .json({ message: "Error deleting API key", error: error.message }); + return res.status(200).json({ + ok: true, + message: "API Key deleted successfully" + }); + + } catch (error) { + console.error('API Key deletion error:', error); + return res.status(500).json({ + ok: false, + message: "Error deleting API key", + code: "server", + error: process.env.NODE_ENV === 'development' ? error : undefined + }); } } ); From 14a391e0bf9ba8d5f3a2ca4d5e427f73e64c667d Mon Sep 17 00:00:00 2001 From: Rohit Date: Wed, 12 Feb 2025 19:14:33 +0530 Subject: [PATCH 081/119] feat: check limit while processing workflow --- server/src/routes/storage.ts | 56 ++++++++++++++++++++---------------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/server/src/routes/storage.ts b/server/src/routes/storage.ts index e159d8cb..399920df 100644 --- a/server/src/routes/storage.ts +++ b/server/src/routes/storage.ts @@ -25,35 +25,43 @@ chromium.use(stealthPlugin()); export const router = Router(); -export const decryptWorkflowActions = async (workflow: any[],): Promise => { - // Create a deep copy to avoid mutating the original workflow - const processedWorkflow = JSON.parse(JSON.stringify(workflow)); +export const processWorkflowActions = async (workflow: any[], checkLimit: boolean = false): Promise => { + const processedWorkflow = JSON.parse(JSON.stringify(workflow)); - // Process each step in the workflow - for (const step of processedWorkflow) { - if (!step.what) continue; - - // Process each action in the step - for (const action of step.what) { - // Only process type and press actions - if ((action.action === 'type' || action.action === 'press') && Array.isArray(action.args) && action.args.length > 1) { - // The second argument contains the encrypted value - const encryptedValue = action.args[1]; - if (typeof encryptedValue === 'string') { - try { - // Decrypt the value and update the args array - action.args[1] = await decrypt(encryptedValue); - } catch (error) { - console.error('Failed to decrypt value:', error); - // Keep the encrypted value if decryption fails + processedWorkflow.workflow.forEach((pair: any) => { + pair.what.forEach((action: any) => { + // Handle limit validation for scrapeList action + if (action.action === 'scrapeList' && checkLimit && Array.isArray(action.args) && action.args.length > 0) { + const scrapeConfig = action.args[0]; + if (scrapeConfig && typeof scrapeConfig === 'object' && 'limit' in scrapeConfig) { + if (typeof scrapeConfig.limit === 'number' && scrapeConfig.limit > 5) { + scrapeConfig.limit = 5; } } } - } - } + + // Handle decryption for type and press actions + if ((action.action === 'type' || action.action === 'press') && Array.isArray(action.args) && action.args.length > 1) { + try { + const encryptedValue = action.args[1]; + if (typeof encryptedValue === 'string') { + const decryptedValue = decrypt(encryptedValue); + action.args[1] = decryptedValue; + } else { + logger.log('error', 'Encrypted value is not a string'); + action.args[1] = ''; + } + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error); + logger.log('error', `Failed to decrypt input value: ${errorMessage}`); + action.args[1] = ''; + } + } + }); + }); return processedWorkflow; -}; +} /** * Logs information about recordings API. @@ -88,7 +96,7 @@ router.get('/recordings/:id', requireSignIn, async (req, res) => { ); if (data?.recording?.workflow) { - data.recording.workflow = await decryptWorkflowActions( + data.recording.workflow = await processWorkflowActions( data.recording.workflow, ); } From 206d760dd13c2561441326b9a7902e9237bdc209 Mon Sep 17 00:00:00 2001 From: Rohit Date: Wed, 12 Feb 2025 22:00:51 +0530 Subject: [PATCH 082/119] feat: accurately get cursor index value for input field --- .../workflow-management/classes/Generator.ts | 94 ++++++++++++++++--- 1 file changed, 81 insertions(+), 13 deletions(-) diff --git a/server/src/workflow-management/classes/Generator.ts b/server/src/workflow-management/classes/Generator.ts index ff36bed9..bd2233ba 100644 --- a/server/src/workflow-management/classes/Generator.ts +++ b/server/src/workflow-management/classes/Generator.ts @@ -431,27 +431,95 @@ export class WorkflowGenerator { // Calculate the exact position within the element const positionAndCursor = await page.evaluate( ({ selector, coords }) => { - const element = document.querySelector(selector); - if (!element) return null; + const getCursorPosition = (element: any, clickX: any) => { + // Get the input's text content + const text = element.value; - const getCursorPosition = (inputElement: HTMLInputElement | HTMLTextAreaElement, clickCoords: Coordinates) => { - const position = inputElement.selectionStart || 0; - return position; - }; + // Create a temporary hidden div to measure text + const mirror = document.createElement('div'); + + // Copy ALL relevant styles that could affect text measurement + const style = window.getComputedStyle(element); + mirror.style.cssText = ` + font: ${style.font}; + line-height: ${style.lineHeight}; + padding: ${style.padding}; + border: ${style.border}; + box-sizing: ${style.boxSizing}; + white-space: ${style.whiteSpace}; + overflow-wrap: ${style.overflowWrap}; + position: absolute; + top: -9999px; + left: -9999px; + width: ${element.offsetWidth}px; + `; + + document.body.appendChild(mirror); + + // Get the element's padding and border widths + const paddingLeft = parseFloat(style.paddingLeft); + const borderLeft = parseFloat(style.borderLeftWidth); + + // Adjust clickX to account for padding and border + const adjustedClickX = clickX - (paddingLeft + borderLeft); + + let bestIndex = 0; + let bestDiff = Infinity; + + // Try each possible cursor position + for (let i = 0; i <= text.length; i++) { + // Create a span for the text before cursor + const textBeforeCursor = text.substring(0, i); + const span = document.createElement('span'); + span.textContent = textBeforeCursor; + mirror.innerHTML = ''; + mirror.appendChild(span); + + // Get the x-position where this character would end + const textWidth = span.getBoundingClientRect().width; + + // Calculate distance from adjusted click to this position + const diff = Math.abs(adjustedClickX - textWidth); + + // If this position is closer to the click, update bestIndex + if (diff < bestDiff) { + bestIndex = i; + bestDiff = diff; + } + } + + // Clean up + document.body.removeChild(mirror); + + // Add debug logging + console.log({ + text, + clickX, + adjustedClickX, + bestIndex, + value: text.substring(0, bestIndex), + nextChar: text[bestIndex] || 'EOL' + }); + + return bestIndex; + }; + + const element = document.querySelector(selector) as HTMLInputElement | HTMLTextAreaElement; + if (!element) return null; const rect = element.getBoundingClientRect(); - const cursorIndex = getCursorPosition(element as HTMLInputElement | HTMLTextAreaElement, coords); + const relativeX = coords.x - rect.left; return { - rect: { - x: rect.left, - y: rect.top - }, - cursorIndex + rect: { + x: rect.left, + y: rect.top + }, + cursorIndex: getCursorPosition(element, relativeX) }; }, { selector, coords: coordinates } - ); + ); if (positionAndCursor) { const relativeX = coordinates.x - positionAndCursor.rect.x; From 945f1200c4d02a168e6996e794f74d65a5d04783 Mon Sep 17 00:00:00 2001 From: Rohit Date: Wed, 12 Feb 2025 22:37:40 +0530 Subject: [PATCH 083/119] feat: optimize press actions to type actions --- .../workflow-management/classes/Generator.ts | 201 ++++++++---------- 1 file changed, 86 insertions(+), 115 deletions(-) diff --git a/server/src/workflow-management/classes/Generator.ts b/server/src/workflow-management/classes/Generator.ts index bd2233ba..ff556359 100644 --- a/server/src/workflow-management/classes/Generator.ts +++ b/server/src/workflow-management/classes/Generator.ts @@ -42,6 +42,13 @@ interface MetaData { isLogin?: boolean; } +interface InputState { + selector: string; + value: string; + type: string; + cursorPosition: number; +} + /** * Workflow generator is used to transform the user's interactions into an automatically * generated correct workflows, using the ability of internal state persistence and @@ -1126,56 +1133,33 @@ export class WorkflowGenerator { * @param workflow The workflow to be optimized. */ private optimizeWorkflow = (workflow: WorkflowFile) => { - // Enhanced input state to include cursor position - let input = { - selector: '', - value: '', - type: '', - actionCounter: 0, - cursorPosition: -1 // Track cursor position, -1 means end of text - }; - - const pushTheOptimizedAction = (pair: WhereWhatPair, index: number) => { - if (input.value.length === 1) { - // Single character - keep as is with waitForLoadState - pair.what.splice(index + 1, 0, { - action: 'waitForLoadState', - args: ['networkidle'], - }); - } else { - // Multiple characters - optimize to type action - pair.what.splice(index - input.actionCounter, input.actionCounter, { - action: 'type', - args: [input.selector, encrypt(input.value), input.type], - }, { - action: 'waitForLoadState', - args: ['networkidle'], - }); - } - }; + // Track state for each input field + const inputStates = new Map(); + // First pass: Process all actions and build final states for (const pair of workflow.workflow) { - for (let i = 0; i < pair.what.length; i++) { - const condition = pair.what[i]; + let currentIndex = 0; + + while (currentIndex < pair.what.length) { + const condition = pair.what[currentIndex]; - // Handle click actions that set cursor position - if (condition.action === 'click' && condition.args?.[1]) { - const cursorIndex = condition.args[1].cursorIndex; + // Handle click actions with cursor positioning + if (condition.action === 'click' && condition.args?.[2]?.cursorIndex !== undefined) { + const selector = condition.args[0]; + const cursorIndex = condition.args[2].cursorIndex; - // If we have pending input, commit it before processing the click - if (input.value.length > 0) { - pushTheOptimizedAction(pair, i); - input = { - selector: '', - value: '', - type: '', - actionCounter: 0, - cursorPosition: -1 - }; - } + let state = inputStates.get(selector) || { + selector, + value: '', + type: 'text', + cursorPosition: -1 + }; - // Update cursor position for next operations - input.cursorPosition = cursorIndex; + state.cursorPosition = cursorIndex; + inputStates.set(selector, state); + + // Remove the click action + pair.what.splice(currentIndex, 1); continue; } @@ -1183,86 +1167,73 @@ export class WorkflowGenerator { if (condition.action === 'press' && condition.args?.[1]) { const [selector, encryptedKey, type] = condition.args; const key = decrypt(encryptedKey); + + let state = inputStates.get(selector) || { + selector, + value: '', + type: type || 'text', + cursorPosition: -1 + }; - // Initialize new input state if selector changes - if (!input.selector || input.selector !== selector) { - if (input.value.length > 0) { - pushTheOptimizedAction(pair, i); - } - input = { - selector, - value: '', - type: type || 'text', - actionCounter: 0, - cursorPosition: -1 - }; - } - - input.actionCounter++; - - // Handle different key types with cursor awareness if (key.length === 1) { - // Insert character at cursor position or append if no cursor set - if (input.cursorPosition === -1) { - // No cursor position set, append to end - input.value += key; + if (state.cursorPosition === -1) { + state.value += key; } else { - // Insert at cursor position - input.value = - input.value.slice(0, input.cursorPosition) + + state.value = + state.value.slice(0, state.cursorPosition) + key + - input.value.slice(input.cursorPosition); - input.cursorPosition++; + state.value.slice(state.cursorPosition); + state.cursorPosition++; } } else if (key === 'Backspace') { - if (input.cursorPosition > 0) { - // Delete character before cursor - input.value = - input.value.slice(0, input.cursorPosition - 1) + - input.value.slice(input.cursorPosition); - input.cursorPosition--; - } else if (input.cursorPosition === -1 && input.value.length > 0) { - // No cursor position set, delete from end - input.value = input.value.slice(0, -1); + if (state.cursorPosition > 0) { + state.value = + state.value.slice(0, state.cursorPosition - 1) + + state.value.slice(state.cursorPosition); + state.cursorPosition--; + } else if (state.cursorPosition === -1 && state.value.length > 0) { + state.value = state.value.slice(0, -1); } - } else if (key !== 'Shift') { - // Handle other special keys - if (input.value.length > 0) { - pushTheOptimizedAction(pair, i); - input = { - selector: '', - value: '', - type: '', - actionCounter: 0, - cursorPosition: -1 - }; + } else if (key === 'Delete') { + if (state.cursorPosition >= 0 && state.cursorPosition < state.value.length) { + state.value = + state.value.slice(0, state.cursorPosition) + + state.value.slice(state.cursorPosition + 1); + } else if (state.cursorPosition === -1 && state.value.length > 0) { + // If no cursor position set, delete at the end + state.value = state.value.slice(0, -1); } } - } else { - // Handle non-text actions - if (input.value.length > 0) { - pushTheOptimizedAction(pair, i); - input = { - selector: '', - value: '', - type: '', - actionCounter: 0, - cursorPosition: -1 - }; - } - } - } - // Clean up any remaining input state - if (input.value.length > 0) { - pushTheOptimizedAction(pair, pair.what.length); - input = { - selector: '', - value: '', - type: '', - actionCounter: 0, - cursorPosition: -1 - }; + inputStates.set(selector, state); + + // Remove the press action + pair.what.splice(currentIndex, 1); + continue; + } + + currentIndex++; + } + } + + // Second pass: Add one type action per selector in the last pair where that selector was used + for (const [selector, state] of inputStates.entries()) { + if (state.value) { + // Find the last pair that used this selector + for (let i = workflow.workflow.length - 1; i >= 0; i--) { + const pair = workflow.workflow[i]; + + // Add type action to the end of the pair + pair.what.push({ + action: 'type', + args: [selector, encrypt(state.value), state.type] + }, { + action: 'waitForLoadState', + args: ['networkidle'] + }); + + break; // Stop after adding to the first pair we find + } } } From afe7c2fced6ae75f2d52792b37ebee4af41508fc Mon Sep 17 00:00:00 2001 From: Rohit Date: Wed, 12 Feb 2025 23:03:49 +0530 Subject: [PATCH 084/119] feat: maintain input type for fields --- .../workflow-management/classes/Generator.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/server/src/workflow-management/classes/Generator.ts b/server/src/workflow-management/classes/Generator.ts index ff556359..fc732e60 100644 --- a/server/src/workflow-management/classes/Generator.ts +++ b/server/src/workflow-management/classes/Generator.ts @@ -1168,12 +1168,18 @@ export class WorkflowGenerator { const [selector, encryptedKey, type] = condition.args; const key = decrypt(encryptedKey); - let state = inputStates.get(selector) || { - selector, - value: '', - type: type || 'text', - cursorPosition: -1 - }; + let state = inputStates.get(selector); + if (!state) { + state = { + selector, + value: '', + type: type || 'text', // Use the type from the press action + cursorPosition: -1 + }; + } else { + // Update type from the press action if it exists + state.type = type || state.type; + } if (key.length === 1) { if (state.cursorPosition === -1) { From 816afa3a813cd63dfaaaf9e916cff4cad8ab7462 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Wed, 12 Feb 2025 23:26:56 +0530 Subject: [PATCH 085/119] fix: login title en --- public/locales/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/locales/en.json b/public/locales/en.json index f072dcc9..919af547 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -50,7 +50,7 @@ "new":"Create Robot", "modal":{ "title":"Enter the URL", - "login_title": "Does this website require login?", + "login_title": "Does this website require logging in?", "label":"URL", "button":"Start Recording" }, From 10ea965cef123a9cdd8332ecbd255b89c4934864 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Wed, 12 Feb 2025 23:30:39 +0530 Subject: [PATCH 086/119] fix: login title german --- public/locales/de.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/locales/de.json b/public/locales/de.json index a4c02b1d..1ff41fba 100644 --- a/public/locales/de.json +++ b/public/locales/de.json @@ -50,7 +50,7 @@ "new": "Roboter erstellen", "modal": { "title": "Geben Sie die URL ein", - "login_title": "Erfordert diese Seite eine Anmeldung?", + "login_title": "Ist für diese Website eine Anmeldung erforderlich?", "label": "URL", "button": "Aufnahme starten" }, From 3316cf2ee4c9515f6a44c285f745df3d3459fda7 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Wed, 12 Feb 2025 23:32:09 +0530 Subject: [PATCH 087/119] fix: login title spanish --- public/locales/es.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/locales/es.json b/public/locales/es.json index 09f6d703..cd79d2cd 100644 --- a/public/locales/es.json +++ b/public/locales/es.json @@ -50,7 +50,7 @@ "new": "Crear Robot", "modal": { "title": "Ingresa la URL", - "login_title": "¿Este sitio requiere inicio de sesión?", + "login_title": "¿Este sitio web requiere iniciar sesión?", "label": "URL", "button": "Comenzar grabación" }, From a4b1a6fdb144c07381b805a3fa27ba6cc7ea0e05 Mon Sep 17 00:00:00 2001 From: Karishma Shukla Date: Thu, 13 Feb 2025 04:21:08 +0530 Subject: [PATCH 088/119] chore: cleanup --- server/src/routes/auth.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts index b7d884a4..47eced63 100644 --- a/server/src/routes/auth.ts +++ b/server/src/routes/auth.ts @@ -17,7 +17,6 @@ router.post("/register", async (req, res) => { try { const { email, password } = req.body; - // Validation checks with translation codes if (!email) { return res.status(400).json({ error: "VALIDATION_ERROR", @@ -32,7 +31,6 @@ router.post("/register", async (req, res) => { }); } - // Check if user exists let userExist = await User.findOne({ raw: true, where: { email } }); if (userExist) { return res.status(400).json({ @@ -43,7 +41,6 @@ router.post("/register", async (req, res) => { const hashedPassword = await hashPassword(password); - // Create user let user: any; try { user = await User.create({ email, password: hashedPassword }); @@ -55,7 +52,6 @@ router.post("/register", async (req, res) => { }); } - // Check JWT secret if (!process.env.JWT_SECRET) { console.log("JWT_SECRET is not defined in the environment"); return res.status(500).json({ @@ -64,7 +60,6 @@ router.post("/register", async (req, res) => { }); } - // Success path const token = jwt.sign({ id: user.id }, process.env.JWT_SECRET as string); user.password = undefined as unknown as string; res.cookie("token", token, { @@ -124,7 +119,6 @@ router.post("/login", async (req, res) => { const token = jwt.sign({ id: user?.id }, process.env.JWT_SECRET as string); - // return user and token to client, exclude hashed password if (user) { user.password = undefined as unknown as string; } From 6a989ad0e431804aa42d5d9e1bc25f44890820c4 Mon Sep 17 00:00:00 2001 From: Rohit Date: Thu, 13 Feb 2025 11:14:38 +0530 Subject: [PATCH 089/119] feat: extract type action creds --- src/components/robot/RobotEdit.tsx | 64 ++++++------------------------ 1 file changed, 12 insertions(+), 52 deletions(-) diff --git a/src/components/robot/RobotEdit.tsx b/src/components/robot/RobotEdit.tsx index f1f79b77..12fd32bd 100644 --- a/src/components/robot/RobotEdit.tsx +++ b/src/components/robot/RobotEdit.tsx @@ -118,6 +118,7 @@ export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettin useEffect(() => { if (robot?.recording?.workflow) { const extractedCredentials = extractInitialCredentials(robot.recording.workflow); + console.log(extractedCredentials); setCredentials(extractedCredentials); setCredentialGroups(groupCredentialsByType(extractedCredentials)); } @@ -126,74 +127,32 @@ export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettin const extractInitialCredentials = (workflow: any[]): Credentials => { const credentials: Credentials = {}; - // Helper function to check if a character is printable - const isPrintableCharacter = (char: string): boolean => { - return char.length === 1 && !!char.match(/^[\x20-\x7E]$/); - }; - // Process each step in the workflow workflow.forEach(step => { if (!step.what) return; - // Keep track of the current input field being processed - let currentSelector = ''; - let currentValue = ''; - let currentType = ''; - // Process actions in sequence to maintain correct text state step.what.forEach((action: any) => { if ( - (action.action === 'type' || action.action === 'press') && + action.action === 'type' && action.args?.length >= 2 && typeof action.args[1] === 'string' ) { const selector: string = action.args[0]; - const character: string = action.args[1]; - const inputType: string = action.args[2] || ''; + const value: string = action.args[1]; + const type: string = action.args[2]; - // Detect `input[type="password"]` - if (!currentType && inputType.toLowerCase() === 'password') { - currentType = 'password'; + if (!credentials[selector]) { + credentials[selector] = { + value: '', + type: '' + }; } - // If we're dealing with a new selector, store the previous one - if (currentSelector && selector !== currentSelector) { - if (!credentials[currentSelector]) { - credentials[currentSelector] = { - value: currentValue, - type: currentType - }; - } else { - credentials[currentSelector].value = currentValue; - } - } - - // Update current tracking variables - if (selector !== currentSelector) { - currentSelector = selector; - currentValue = credentials[selector]?.value || ''; - currentType = inputType || credentials[selector]?.type || ''; - } - - // Handle different types of key actions - if (character === 'Backspace') { - // Remove the last character when backspace is pressed - currentValue = currentValue.slice(0, -1); - } else if (isPrintableCharacter(character)) { - // Add the character to the current value - currentValue += character; - } - // Note: We ignore other special keys like 'Shift', 'Enter', etc. + credentials[selector].value = value; + credentials[selector].type = type; } }); - - // Store the final state of the last processed selector - if (currentSelector) { - credentials[currentSelector] = { - value: currentValue, - type: currentType - }; - } }); return credentials; @@ -224,6 +183,7 @@ export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettin const getRobot = async () => { if (recordingId) { const robot = await getStoredRecording(recordingId); + console.log("ROBOT:", robot); setRobot(robot); } else { notify('error', t('robot_edit.notifications.update_failed')); From 4c21e2794a4e87e8b6e91dd97f827064c62ca526 Mon Sep 17 00:00:00 2001 From: Rohit Date: Thu, 13 Feb 2025 11:15:51 +0530 Subject: [PATCH 090/119] feat: console log cleanup --- src/components/robot/RobotEdit.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/robot/RobotEdit.tsx b/src/components/robot/RobotEdit.tsx index 12fd32bd..b4b271bc 100644 --- a/src/components/robot/RobotEdit.tsx +++ b/src/components/robot/RobotEdit.tsx @@ -183,7 +183,6 @@ export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettin const getRobot = async () => { if (recordingId) { const robot = await getStoredRecording(recordingId); - console.log("ROBOT:", robot); setRobot(robot); } else { notify('error', t('robot_edit.notifications.update_failed')); From b7ce40c8af0b8f7f942b02b4c05f6c214b9a08aa Mon Sep 17 00:00:00 2001 From: Rohit Date: Thu, 13 Feb 2025 11:58:02 +0530 Subject: [PATCH 091/119] feat: update workflow type actions, update robot --- server/src/routes/storage.ts | 69 +++++++++++++++--------------------- 1 file changed, 29 insertions(+), 40 deletions(-) diff --git a/server/src/routes/storage.ts b/server/src/routes/storage.ts index 01f1ca6b..aa63798f 100644 --- a/server/src/routes/storage.ts +++ b/server/src/routes/storage.ts @@ -167,51 +167,26 @@ function updateTypeActionsInWorkflow(workflow: any[], credentials: Credentials) return workflow.map(step => { if (!step.what) return step; - const indicesToRemove = new Set(); - step.what.forEach((action: any, index: number) => { - if (!action.action || !action.args?.[0]) return; - - if ((action.action === 'type' || action.action === 'press') && credentials[action.args[0]]) { - indicesToRemove.add(index); - - if (step.what[index + 1]?.action === 'waitForLoadState') { - indicesToRemove.add(index + 1); - } - } - }); - - const filteredWhat = step.what.filter((_: any, index: number) => !indicesToRemove.has(index)); - - Object.entries(credentials).forEach(([selector, credentialInfo]) => { - const clickIndex = filteredWhat.findIndex((action: any) => - action.action === 'click' && action.args?.[0] === selector - ); - - if (clickIndex !== -1) { - const chars = credentialInfo.value.split(''); + step.what = step.what.map((action: any) => { + if (action.action === 'type' && action.args?.length >= 2) { + const selector = action.args[0]; - chars.forEach((char, i) => { - filteredWhat.splice(clickIndex + 1 + (i * 2), 0, { - action: 'type', + if (credentials[selector]) { + return { + ...action, args: [ selector, - encrypt(char), - credentialInfo.type + encrypt(credentials[selector].value), + credentials[selector].type ] - }); - - filteredWhat.splice(clickIndex + 2 + (i * 2), 0, { - action: 'waitForLoadState', - args: ['networkidle'] - }); - }); + }; + } } + + return action; }); - return { - ...step, - what: filteredWhat - }; + return step; }); } @@ -281,9 +256,23 @@ router.put('/recordings/:id', requireSignIn, async (req: AuthenticatedRequest, r } } - robot.set('recording', { ...robot.recording, workflow }); + const updates: any = { + recording: { + ...robot.recording, + workflow + } + }; - await robot.save(); + if (name) { + updates.recording_meta = { + ...robot.recording_meta, + name + }; + } + + await Robot.update(updates, { + where: { 'recording_meta.id': id } + }); const updatedRobot = await Robot.findOne({ where: { 'recording_meta.id': id } }); From 81b917b3976545c3cafe4143eb76ac8b29db4a1a Mon Sep 17 00:00:00 2001 From: Rohit Date: Thu, 13 Feb 2025 12:05:02 +0530 Subject: [PATCH 092/119] feat: console log cleanup --- src/components/robot/RobotEdit.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/robot/RobotEdit.tsx b/src/components/robot/RobotEdit.tsx index b4b271bc..86fc5974 100644 --- a/src/components/robot/RobotEdit.tsx +++ b/src/components/robot/RobotEdit.tsx @@ -118,7 +118,6 @@ export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettin useEffect(() => { if (robot?.recording?.workflow) { const extractedCredentials = extractInitialCredentials(robot.recording.workflow); - console.log(extractedCredentials); setCredentials(extractedCredentials); setCredentialGroups(groupCredentialsByType(extractedCredentials)); } From 8369714a152d8cb989669cd1f8fd00de3fe53cd5 Mon Sep 17 00:00:00 2001 From: Rohit Date: Thu, 13 Feb 2025 12:08:33 +0530 Subject: [PATCH 093/119] feat: comments cleanup --- .../workflow-management/classes/Generator.ts | 37 +------------------ 1 file changed, 2 insertions(+), 35 deletions(-) diff --git a/server/src/workflow-management/classes/Generator.ts b/server/src/workflow-management/classes/Generator.ts index fc732e60..a5d92d4e 100644 --- a/server/src/workflow-management/classes/Generator.ts +++ b/server/src/workflow-management/classes/Generator.ts @@ -435,17 +435,13 @@ export class WorkflowGenerator { } if ((elementInfo?.tagName === 'INPUT' || elementInfo?.tagName === 'TEXTAREA') && selector) { - // Calculate the exact position within the element const positionAndCursor = await page.evaluate( ({ selector, coords }) => { const getCursorPosition = (element: any, clickX: any) => { - // Get the input's text content const text = element.value; - // Create a temporary hidden div to measure text const mirror = document.createElement('div'); - // Copy ALL relevant styles that could affect text measurement const style = window.getComputedStyle(element); mirror.style.cssText = ` font: ${style.font}; @@ -463,51 +459,33 @@ export class WorkflowGenerator { document.body.appendChild(mirror); - // Get the element's padding and border widths const paddingLeft = parseFloat(style.paddingLeft); const borderLeft = parseFloat(style.borderLeftWidth); - // Adjust clickX to account for padding and border const adjustedClickX = clickX - (paddingLeft + borderLeft); let bestIndex = 0; let bestDiff = Infinity; - // Try each possible cursor position for (let i = 0; i <= text.length; i++) { - // Create a span for the text before cursor const textBeforeCursor = text.substring(0, i); const span = document.createElement('span'); span.textContent = textBeforeCursor; mirror.innerHTML = ''; mirror.appendChild(span); - // Get the x-position where this character would end const textWidth = span.getBoundingClientRect().width; - // Calculate distance from adjusted click to this position const diff = Math.abs(adjustedClickX - textWidth); - // If this position is closer to the click, update bestIndex if (diff < bestDiff) { bestIndex = i; bestDiff = diff; } } - // Clean up document.body.removeChild(mirror); - // Add debug logging - console.log({ - text, - clickX, - adjustedClickX, - bestIndex, - value: text.substring(0, bestIndex), - nextChar: text[bestIndex] || 'EOL' - }); - return bestIndex; }; @@ -1133,17 +1111,14 @@ export class WorkflowGenerator { * @param workflow The workflow to be optimized. */ private optimizeWorkflow = (workflow: WorkflowFile) => { - // Track state for each input field const inputStates = new Map(); - // First pass: Process all actions and build final states for (const pair of workflow.workflow) { let currentIndex = 0; while (currentIndex < pair.what.length) { const condition = pair.what[currentIndex]; - // Handle click actions with cursor positioning if (condition.action === 'click' && condition.args?.[2]?.cursorIndex !== undefined) { const selector = condition.args[0]; const cursorIndex = condition.args[2].cursorIndex; @@ -1158,12 +1133,10 @@ export class WorkflowGenerator { state.cursorPosition = cursorIndex; inputStates.set(selector, state); - // Remove the click action pair.what.splice(currentIndex, 1); continue; } - // Handle text input and editing if (condition.action === 'press' && condition.args?.[1]) { const [selector, encryptedKey, type] = condition.args; const key = decrypt(encryptedKey); @@ -1173,11 +1146,10 @@ export class WorkflowGenerator { state = { selector, value: '', - type: type || 'text', // Use the type from the press action + type: type || 'text', cursorPosition: -1 }; } else { - // Update type from the press action if it exists state.type = type || state.type; } @@ -1206,14 +1178,12 @@ export class WorkflowGenerator { state.value.slice(0, state.cursorPosition) + state.value.slice(state.cursorPosition + 1); } else if (state.cursorPosition === -1 && state.value.length > 0) { - // If no cursor position set, delete at the end state.value = state.value.slice(0, -1); } } inputStates.set(selector, state); - // Remove the press action pair.what.splice(currentIndex, 1); continue; } @@ -1222,14 +1192,11 @@ export class WorkflowGenerator { } } - // Second pass: Add one type action per selector in the last pair where that selector was used for (const [selector, state] of inputStates.entries()) { if (state.value) { - // Find the last pair that used this selector for (let i = workflow.workflow.length - 1; i >= 0; i--) { const pair = workflow.workflow[i]; - // Add type action to the end of the pair pair.what.push({ action: 'type', args: [selector, encrypt(state.value), state.type] @@ -1238,7 +1205,7 @@ export class WorkflowGenerator { args: ['networkidle'] }); - break; // Stop after adding to the first pair we find + break; } } } From 5207d7da6f90b0d67130825dafedc0fec6c3764f Mon Sep 17 00:00:00 2001 From: Rohit Date: Thu, 13 Feb 2025 12:11:04 +0530 Subject: [PATCH 094/119] feat: default to text if type not specified --- src/components/robot/RobotEdit.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/robot/RobotEdit.tsx b/src/components/robot/RobotEdit.tsx index 86fc5974..1f76c725 100644 --- a/src/components/robot/RobotEdit.tsx +++ b/src/components/robot/RobotEdit.tsx @@ -139,7 +139,7 @@ export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettin ) { const selector: string = action.args[0]; const value: string = action.args[1]; - const type: string = action.args[2]; + const type: string = action.args[2] || 'text'; if (!credentials[selector]) { credentials[selector] = { From cd574194b0e4a5ad7b0667262abb57e007bf5151 Mon Sep 17 00:00:00 2001 From: Rohit Date: Thu, 13 Feb 2025 12:23:58 +0530 Subject: [PATCH 095/119] feat: console log cleanup --- .../workflow-management/classes/Generator.ts | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/server/src/workflow-management/classes/Generator.ts b/server/src/workflow-management/classes/Generator.ts index a5d92d4e..28b8b361 100644 --- a/server/src/workflow-management/classes/Generator.ts +++ b/server/src/workflow-management/classes/Generator.ts @@ -768,23 +768,6 @@ export class WorkflowGenerator { * @returns {Promise} */ public saveNewWorkflow = async (fileName: string, userId: number, isLogin: boolean) => { - for (const pair of this.workflowRecord.workflow) { - for (let i = 0; i < pair.what.length; i++) { - const condition = pair.what[i]; - - if (condition.action === 'press' && condition.args) { - const [selector, encryptedKey, type] = condition.args; - const key = decrypt(encryptedKey); - - console.log(`Selector: ${selector}, Key: ${key}`); - } - - if (condition.action === 'click' && condition.args) { - console.log("Click args: ", condition.args); - } - } - } - const recording = this.optimizeWorkflow(this.workflowRecord); try { this.recordingMeta = { From c6f5479358bd7d47192761ec66105312967133a3 Mon Sep 17 00:00:00 2001 From: Rohit Date: Thu, 13 Feb 2025 15:50:13 +0530 Subject: [PATCH 096/119] feat: add support for full and partial word type actions --- src/components/robot/RobotEdit.tsx | 120 +++++++++++++++++++++++------ 1 file changed, 97 insertions(+), 23 deletions(-) diff --git a/src/components/robot/RobotEdit.tsx b/src/components/robot/RobotEdit.tsx index 1f76c725..6e8868de 100644 --- a/src/components/robot/RobotEdit.tsx +++ b/src/components/robot/RobotEdit.tsx @@ -123,39 +123,113 @@ export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettin } }, [robot]); - const extractInitialCredentials = (workflow: any[]): Credentials => { + function extractInitialCredentials(workflow: any[]): Credentials { const credentials: Credentials = {}; - - // Process each step in the workflow + + const isPrintableCharacter = (char: string): boolean => { + return char.length === 1 && !!char.match(/^[\x20-\x7E]$/); + }; + workflow.forEach(step => { if (!step.what) return; - - // Process actions in sequence to maintain correct text state - step.what.forEach((action: any) => { - if ( - action.action === 'type' && - action.args?.length >= 2 && - typeof action.args[1] === 'string' - ) { - const selector: string = action.args[0]; - const value: string = action.args[1]; - const type: string = action.args[2] || 'text'; - + + let currentSelector = ''; + let currentValue = ''; + let currentType = ''; + let i = 0; + + while (i < step.what.length) { + const action = step.what[i]; + + if (!action.action || !action.args?.[0]) { + i++; + continue; + } + + const selector = action.args[0]; + + // Handle full word type actions first + if (action.action === 'type' && + action.args?.length >= 2 && + typeof action.args[1] === 'string' && + action.args[1].length > 1) { + if (!credentials[selector]) { credentials[selector] = { - value: '', - type: '' + value: action.args[1], + type: action.args[2] || 'text' }; } - - credentials[selector].value = value; - credentials[selector].type = type; + i++; + continue; } - }); + + // Handle character-by-character sequences (both type and press) + if ((action.action === 'type' || action.action === 'press') && + action.args?.length >= 2 && + typeof action.args[1] === 'string') { + + if (selector !== currentSelector) { + if (currentSelector && currentValue) { + credentials[currentSelector] = { + value: currentValue, + type: currentType || 'text' + }; + } + currentSelector = selector; + currentValue = credentials[selector]?.value || ''; + currentType = action.args[2] || credentials[selector]?.type || 'text'; + } + + const character = action.args[1]; + + if (isPrintableCharacter(character)) { + currentValue += character; + } else if (character === 'Backspace') { + currentValue = currentValue.slice(0, -1); + } + + if (!currentType && action.args[2]?.toLowerCase() === 'password') { + currentType = 'password'; + } + + let j = i + 1; + while (j < step.what.length) { + const nextAction = step.what[j]; + if (!nextAction.action || !nextAction.args?.[0] || + nextAction.args[0] !== selector || + (nextAction.action !== 'type' && nextAction.action !== 'press')) { + break; + } + if (nextAction.args[1] === 'Backspace') { + currentValue = currentValue.slice(0, -1); + } else if (isPrintableCharacter(nextAction.args[1])) { + currentValue += nextAction.args[1]; + } + j++; + } + + credentials[currentSelector] = { + value: currentValue, + type: currentType + }; + + i = j; + } else { + i++; + } + } + + if (currentSelector && currentValue) { + credentials[currentSelector] = { + value: currentValue, + type: currentType || 'text' + }; + } }); - + return credentials; - }; + } const groupCredentialsByType = (credentials: Credentials): GroupedCredentials => { return Object.entries(credentials).reduce((acc: GroupedCredentials, [selector, info]) => { From ff21f5355b41d4c2f8c0c21b16db499f93f22926 Mon Sep 17 00:00:00 2001 From: Rohit Date: Thu, 13 Feb 2025 16:13:42 +0530 Subject: [PATCH 097/119] feat: fallback to update type actions --- server/src/routes/storage.ts | 99 +++++++++++++++++++++++++++++------- 1 file changed, 81 insertions(+), 18 deletions(-) diff --git a/server/src/routes/storage.ts b/server/src/routes/storage.ts index aa63798f..7e5b3638 100644 --- a/server/src/routes/storage.ts +++ b/server/src/routes/storage.ts @@ -163,30 +163,93 @@ interface Credentials { [key: string]: CredentialInfo; } -function updateTypeActionsInWorkflow(workflow: any[], credentials: Credentials) { +function handleWorkflowActions(workflow: any[], credentials: Credentials) { + // Process one step at a time to avoid memory issues return workflow.map(step => { if (!step.what) return step; - step.what = step.what.map((action: any) => { - if (action.action === 'type' && action.args?.length >= 2) { - const selector = action.args[0]; + // Use a more memory-efficient approach + const newWhat: any[] = []; + const processedSelectors = new Set(); + + for (let i = 0; i < step.what.length; i++) { + const action = step.what[i]; + + // Skip invalid actions + if (!action?.action || !action?.args?.[0]) { + newWhat.push(action); + continue; + } + + const selector = action.args[0]; + const credential = credentials[selector]; + + // Handle non-credential actions + if (!credential) { + newWhat.push(action); + continue; + } + + // Handle credential-related actions + if (action.action === 'click') { + newWhat.push(action); - if (credentials[selector]) { - return { - ...action, - args: [ - selector, - encrypt(credentials[selector].value), - credentials[selector].type - ] - }; + // If we haven't processed this selector and there's a following type/press action + if (!processedSelectors.has(selector) && + i + 1 < step.what.length && + (step.what[i + 1].action === 'type' || step.what[i + 1].action === 'press')) { + + // Add type action + newWhat.push({ + action: 'type', + args: [selector, encrypt(credential.value), credential.type] + }); + + // Add single waitForLoadState + newWhat.push({ + action: 'waitForLoadState', + args: ['networkidle'] + }); + + processedSelectors.add(selector); + + // Skip subsequent type/press/waitForLoadState actions for this selector + while (i + 1 < step.what.length && + (step.what[i + 1].action === 'type' || + step.what[i + 1].action === 'press' || + step.what[i + 1].action === 'waitForLoadState')) { + i++; + } + } + } else if ((action.action === 'type' || action.action === 'press') && + !processedSelectors.has(selector)) { + // Handle standalone type/press action + newWhat.push({ + action: 'type', + args: [selector, encrypt(credential.value), credential.type] + }); + + newWhat.push({ + action: 'waitForLoadState', + args: ['networkidle'] + }); + + processedSelectors.add(selector); + + // Skip subsequent type/press/waitForLoadState actions for this selector + while (i + 1 < step.what.length && + (step.what[i + 1].action === 'type' || + step.what[i + 1].action === 'press' || + step.what[i + 1].action === 'waitForLoadState')) { + i++; } } - - return action; - }); + } - return step; + return { + ...step, + what: newWhat + }; }); } @@ -218,7 +281,7 @@ router.put('/recordings/:id', requireSignIn, async (req: AuthenticatedRequest, r let workflow = [...robot.recording.workflow]; // Create a copy of the workflow if (credentials) { - workflow = updateTypeActionsInWorkflow(workflow, credentials); + workflow = handleWorkflowActions(workflow, credentials); } // Update the limit From 99696aaba2aabef1421a362930c1afd704427eb9 Mon Sep 17 00:00:00 2001 From: Rohit Date: Thu, 13 Feb 2025 16:15:27 +0530 Subject: [PATCH 098/119] feat: comment cleanup --- server/src/routes/storage.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/server/src/routes/storage.ts b/server/src/routes/storage.ts index 7e5b3638..ea1fce38 100644 --- a/server/src/routes/storage.ts +++ b/server/src/routes/storage.ts @@ -164,18 +164,15 @@ interface Credentials { } function handleWorkflowActions(workflow: any[], credentials: Credentials) { - // Process one step at a time to avoid memory issues return workflow.map(step => { if (!step.what) return step; - // Use a more memory-efficient approach const newWhat: any[] = []; const processedSelectors = new Set(); for (let i = 0; i < step.what.length; i++) { const action = step.what[i]; - // Skip invalid actions if (!action?.action || !action?.args?.[0]) { newWhat.push(action); continue; @@ -184,28 +181,23 @@ function handleWorkflowActions(workflow: any[], credentials: Credentials) { const selector = action.args[0]; const credential = credentials[selector]; - // Handle non-credential actions if (!credential) { newWhat.push(action); continue; } - // Handle credential-related actions if (action.action === 'click') { newWhat.push(action); - // If we haven't processed this selector and there's a following type/press action if (!processedSelectors.has(selector) && i + 1 < step.what.length && (step.what[i + 1].action === 'type' || step.what[i + 1].action === 'press')) { - // Add type action newWhat.push({ action: 'type', args: [selector, encrypt(credential.value), credential.type] }); - // Add single waitForLoadState newWhat.push({ action: 'waitForLoadState', args: ['networkidle'] @@ -213,7 +205,6 @@ function handleWorkflowActions(workflow: any[], credentials: Credentials) { processedSelectors.add(selector); - // Skip subsequent type/press/waitForLoadState actions for this selector while (i + 1 < step.what.length && (step.what[i + 1].action === 'type' || step.what[i + 1].action === 'press' || @@ -223,7 +214,6 @@ function handleWorkflowActions(workflow: any[], credentials: Credentials) { } } else if ((action.action === 'type' || action.action === 'press') && !processedSelectors.has(selector)) { - // Handle standalone type/press action newWhat.push({ action: 'type', args: [selector, encrypt(credential.value), credential.type] From a90ee270f2edf22ce56c6cbf5e831e98510a52ba Mon Sep 17 00:00:00 2001 From: amhsirak Date: Fri, 14 Feb 2025 23:35:44 +0530 Subject: [PATCH 099/119] feat: create an account --- public/locales/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/locales/en.json b/public/locales/en.json index 919af547..883892a6 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -20,7 +20,7 @@ } }, "register": { - "title": "Register Account", + "title": "Create An Account", "email": "Enter Work Email", "password": "Password", "button": "Register", From 4d058eb8292c93ac0d41e12faee180249cb5961c Mon Sep 17 00:00:00 2001 From: Rohit Date: Sat, 15 Feb 2025 12:17:04 +0530 Subject: [PATCH 100/119] feat: add translation for logout notifs --- public/locales/de.json | 13 +++++++++++++ public/locales/en.json | 13 +++++++++++++ public/locales/es.json | 13 +++++++++++++ public/locales/ja.json | 13 +++++++++++++ public/locales/zh.json | 13 +++++++++++++ 5 files changed, 65 insertions(+) diff --git a/public/locales/de.json b/public/locales/de.json index 1ff41fba..7314b677 100644 --- a/public/locales/de.json +++ b/public/locales/de.json @@ -487,6 +487,19 @@ }, "navbar": { "project_name": "Maxun", + "notifications": { + "success": { + "logout": "Erfolgreich abgemeldet" + }, + "errors": { + "logout": { + "unauthorized": "Sie sind nicht berechtigt, diese Aktion durchzuführen", + "server": "Serverfehler beim Abmelden", + "network": "Netzwerkfehler beim Abmelden", + "unknown": "Ein unerwarteter Fehler ist beim Abmelden aufgetreten" + } + } + }, "upgrade": { "button": "Upgrade", "modal": { diff --git a/public/locales/en.json b/public/locales/en.json index 919af547..cfaf9ef4 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -498,6 +498,19 @@ }, "navbar": { "project_name": "Maxun", + "notifications": { + "success": { + "logout": "Logged out successfully" + }, + "errors": { + "logout": { + "unauthorized": "You are not authorized to perform this action", + "server": "Server error occurred during logout", + "network": "Network error while logging out", + "unknown": "An unexpected error occurred during logout" + } + } + }, "upgrade": { "button": "Upgrade", "modal": { diff --git a/public/locales/es.json b/public/locales/es.json index cd79d2cd..ca4edc5d 100644 --- a/public/locales/es.json +++ b/public/locales/es.json @@ -488,6 +488,19 @@ }, "navbar": { "project_name": "Maxun", + "notifications": { + "success": { + "logout": "Sesión cerrada exitosamente" + }, + "errors": { + "logout": { + "unauthorized": "No estás autorizado para realizar esta acción", + "server": "Error del servidor durante el cierre de sesión", + "network": "Error de red al cerrar sesión", + "unknown": "Ocurrió un error inesperado al cerrar sesión" + } + } + }, "upgrade": { "button": "Actualizar", "modal": { diff --git a/public/locales/ja.json b/public/locales/ja.json index d2fe42ea..a4a66143 100644 --- a/public/locales/ja.json +++ b/public/locales/ja.json @@ -488,6 +488,19 @@ }, "navbar": { "project_name": "Maxun", + "notifications": { + "success": { + "logout": "ログアウトに成功しました" + }, + "errors": { + "logout": { + "unauthorized": "この操作を実行する権限がありません", + "server": "ログアウト中にサーバーエラーが発生しました", + "network": "ログアウト中にネットワークエラーが発生しました", + "unknown": "ログアウト中に予期せぬエラーが発生しました" + } + } + }, "upgrade": { "button": "アップグレード", "modal": { diff --git a/public/locales/zh.json b/public/locales/zh.json index e7c58660..c92ed186 100644 --- a/public/locales/zh.json +++ b/public/locales/zh.json @@ -488,6 +488,19 @@ }, "navbar": { "project_name": "Maxun", + "notifications": { + "success": { + "logout": "退出登录成功" + }, + "errors": { + "logout": { + "unauthorized": "您没有执行此操作的权限", + "server": "退出登录时发生服务器错误", + "network": "退出登录时发生网络错误", + "unknown": "退出登录时发生未知错误" + } + } + }, "upgrade": { "button": "升级", "modal": { From f3f81d6c5298646410fee366a2a8248ec9290d56 Mon Sep 17 00:00:00 2001 From: Rohit Date: Sat, 15 Feb 2025 12:18:17 +0530 Subject: [PATCH 101/119] feat: better logout error messages --- src/components/dashboard/NavBar.tsx | 38 +++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/src/components/dashboard/NavBar.tsx b/src/components/dashboard/NavBar.tsx index 8a9b1e9c..2456d013 100644 --- a/src/components/dashboard/NavBar.tsx +++ b/src/components/dashboard/NavBar.tsx @@ -108,11 +108,39 @@ export const NavBar: React.FC = ({ }; const logout = async () => { - dispatch({ type: "LOGOUT" }); - window.localStorage.removeItem("user"); - const { data } = await axios.get(`${apiUrl}/auth/logout`); - notify("success", data.message); - navigate("/login"); + try { + const { data } = await axios.get(`${apiUrl}/auth/logout`); + if (data.ok) { + dispatch({ type: "LOGOUT" }); + window.localStorage.removeItem("user"); + notify('success', t('navbar.notifications.success.logout')); + navigate("/login"); + } + } catch (error: any) { + const status = error.response?.status; + let errorKey = 'unknown'; + + switch (status) { + case 401: + errorKey = 'unauthorized'; + break; + case 500: + errorKey = 'server'; + break; + default: + if (error.message?.includes('Network Error')) { + errorKey = 'network'; + } + } + + notify( + 'error', + t(`navbar.notifications.errors.logout.${errorKey}`, { + error: error.response?.data?.message || error.message + }) + ); + navigate("/login"); + } }; const goToMainMenu = async () => { From 18787317da278458536edf8ab98944ebddcfbcee Mon Sep 17 00:00:00 2001 From: Rohit Date: Sat, 15 Feb 2025 12:18:54 +0530 Subject: [PATCH 102/119] feat: add code for error messages --- server/src/routes/auth.ts | 38 +++++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts index 47eced63..f18ee8c1 100644 --- a/server/src/routes/auth.ts +++ b/server/src/routes/auth.ts @@ -140,14 +140,38 @@ router.post("/login", async (req, res) => { } }); -router.get("/logout", async (req, res) => { - try { - res.clearCookie("token"); - return res.json({ message: "Logout successful" }); - } catch (error: any) { - res.status(500).send(`Could not logout user - ${error.message}`); +router.get( + "/logout", + requireSignIn, + async (req: Request, res) => { + const authenticatedReq = req as AuthenticatedRequest; + try { + if (!authenticatedReq.user) { + return res.status(401).json({ + ok: false, + message: "Unauthorized", + code: "unauthorized" + }); + } + + res.clearCookie("token"); + + return res.status(200).json({ + ok: true, + message: "Logged out successfully", + code: "success" + }); + } catch (error) { + console.error('Logout error:', error); + return res.status(500).json({ + ok: false, + message: "Error during logout", + code: "server", + error: process.env.NODE_ENV === 'development' ? error : undefined + }); + } } -}); +); router.get( "/current-user", From a5c100f1744919387846a38894550204805d2db7 Mon Sep 17 00:00:00 2001 From: Rohit Date: Thu, 20 Feb 2025 13:18:03 +0530 Subject: [PATCH 103/119] fix: add error handling for optimize --- server/src/workflow-management/selector.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 3f44a01e..20a6a529 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -1092,12 +1092,16 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { newPath.splice(i, 1); const newPathKey = selector(newPath); if (scope.visited.has(newPathKey)) { - return; + continue; } - if (unique(newPath) && same(newPath, input)) { - yield newPath; - scope.visited.set(newPathKey, true); - yield* optimize(newPath, input, scope); + try { + if (unique(newPath) && same(newPath, input)) { + yield newPath; + scope.visited.set(newPathKey, true); + yield* optimize(newPath, input, scope); + } + } catch (e: any) { + continue; } } } From cbe8d4fee89504f1db81114eda690b230958371b Mon Sep 17 00:00:00 2001 From: Rohit Date: Thu, 20 Feb 2025 19:04:43 +0530 Subject: [PATCH 104/119] fix: add selector check for state --- maxun-core/src/interpret.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/maxun-core/src/interpret.ts b/maxun-core/src/interpret.ts index 7fc4d17a..88047128 100644 --- a/maxun-core/src/interpret.ts +++ b/maxun-core/src/interpret.ts @@ -286,6 +286,12 @@ export default class Interpreter extends EventEmitter { ? arrayToObject(superset[key]) : superset[key]; + if (key === 'selectors' && Array.isArray(value) && Array.isArray(superset[key])) { + return value.some(selector => + (superset[key] as any[]).includes(selector) + ); + } + // Every `subset` key must exist in the `superset` and // have the same value (strict equality), or subset[key] <= superset[key] return parsedSuperset[key] From 2033df72f6a8616a9880a9451a3907342d430f45 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Fri, 21 Feb 2025 15:52:38 +0530 Subject: [PATCH 105/119] feat: set cursor to pointer --- src/components/dashboard/NavBar.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/dashboard/NavBar.tsx b/src/components/dashboard/NavBar.tsx index 8a9b1e9c..7408ec79 100644 --- a/src/components/dashboard/NavBar.tsx +++ b/src/components/dashboard/NavBar.tsx @@ -205,6 +205,7 @@ export const NavBar: React.FC = ({
{t('navbar.project_name')}
From dfecc521f4ec7622574e5e1be3343af9bb7ef5be Mon Sep 17 00:00:00 2001 From: amhsirak Date: Fri, 21 Feb 2025 15:52:55 +0530 Subject: [PATCH 106/119] fix: format --- src/components/dashboard/NavBar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/dashboard/NavBar.tsx b/src/components/dashboard/NavBar.tsx index 7408ec79..f6c7102c 100644 --- a/src/components/dashboard/NavBar.tsx +++ b/src/components/dashboard/NavBar.tsx @@ -205,7 +205,7 @@ export const NavBar: React.FC = ({
{t('navbar.project_name')}
From 87f9b4d07c0cb09d3e0eb474e774dc8fe63233eb Mon Sep 17 00:00:00 2001 From: amhsirak Date: Fri, 21 Feb 2025 15:53:37 +0530 Subject: [PATCH 107/119] feat: onclick navigate to / --- src/components/dashboard/NavBar.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/dashboard/NavBar.tsx b/src/components/dashboard/NavBar.tsx index f6c7102c..a437ba61 100644 --- a/src/components/dashboard/NavBar.tsx +++ b/src/components/dashboard/NavBar.tsx @@ -206,7 +206,8 @@ export const NavBar: React.FC = ({ display: 'flex', justifyContent: 'flex-start', cursor: 'pointer' - }}> + }} + onClick={() => navigate('/')}>
{t('navbar.project_name')}
Date: Fri, 21 Feb 2025 15:53:45 +0530 Subject: [PATCH 108/119] fix: format --- src/components/dashboard/NavBar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/dashboard/NavBar.tsx b/src/components/dashboard/NavBar.tsx index a437ba61..8f951646 100644 --- a/src/components/dashboard/NavBar.tsx +++ b/src/components/dashboard/NavBar.tsx @@ -207,7 +207,7 @@ export const NavBar: React.FC = ({ justifyContent: 'flex-start', cursor: 'pointer' }} - onClick={() => navigate('/')}> + onClick={() => navigate('/')}>
{t('navbar.project_name')}
Date: Sat, 22 Feb 2025 14:41:57 +0530 Subject: [PATCH 109/119] fix: rm auth check for logout --- server/src/routes/auth.ts | 47 +++++++++++++-------------------------- 1 file changed, 15 insertions(+), 32 deletions(-) diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts index f18ee8c1..9396a5d4 100644 --- a/server/src/routes/auth.ts +++ b/server/src/routes/auth.ts @@ -140,22 +140,9 @@ router.post("/login", async (req, res) => { } }); -router.get( - "/logout", - requireSignIn, - async (req: Request, res) => { - const authenticatedReq = req as AuthenticatedRequest; +router.get("/logout", async (req, res) => { try { - if (!authenticatedReq.user) { - return res.status(401).json({ - ok: false, - message: "Unauthorized", - code: "unauthorized" - }); - } - res.clearCookie("token"); - return res.status(200).json({ ok: true, message: "Logged out successfully", @@ -176,13 +163,12 @@ router.get( router.get( "/current-user", requireSignIn, - async (req: Request, res) => { - const authenticatedReq = req as AuthenticatedRequest; + async (req: AuthenticatedRequest, res) => { try { - if (!authenticatedReq.user) { + if (!req.user) { return res.status(401).json({ ok: false, error: "Unauthorized" }); } - const user = await User.findByPk(authenticatedReq.user.id, { + const user = await User.findByPk(req.user.id, { attributes: { exclude: ["password"] }, }); if (!user) { @@ -205,7 +191,7 @@ router.get( router.get( "/user/:id", requireSignIn, - async (req: Request, res) => { + async (req: AuthenticatedRequest, res) => { try { const { id } = req.params; if (!id) { @@ -234,13 +220,12 @@ router.get( router.post( "/generate-api-key", requireSignIn, - async (req: Request, res) => { - const authenticatedReq = req as AuthenticatedRequest; + async (req: AuthenticatedRequest, res) => { try { - if (!authenticatedReq.user) { + if (!req.user) { return res.status(401).json({ ok: false, error: "Unauthorized" }); } - const user = await User.findByPk(authenticatedReq.user.id, { + const user = await User.findByPk(req.user.id, { attributes: { exclude: ["password"] }, }); @@ -275,14 +260,13 @@ router.post( router.get( "/api-key", requireSignIn, - async (req: Request, res) => { - const authenticatedReq = req as AuthenticatedRequest; + async (req: AuthenticatedRequest, res) => { try { - if (!authenticatedReq.user) { + if (!req.user) { return res.status(401).json({ ok: false, error: "Unauthorized" }); } - const user = await User.findByPk(authenticatedReq.user.id, { + const user = await User.findByPk(req.user.id, { raw: true, attributes: ["api_key"], }); @@ -304,14 +288,13 @@ router.get( router.delete( "/delete-api-key", requireSignIn, - async (req: Request, res) => { - const authenticatedReq = req as AuthenticatedRequest; - if (!authenticatedReq.user) { + async (req: AuthenticatedRequest, res) => { + if (!req.user) { return res.status(401).send({ error: "Unauthorized" }); } try { - const user = await User.findByPk(authenticatedReq.user.id, { raw: true }); + const user = await User.findByPk(req.user.id, { raw: true }); if (!user) { return res.status(404).json({ message: "User not found" }); @@ -321,7 +304,7 @@ router.delete( return res.status(404).json({ message: "API Key not found" }); } - await User.update({ api_key: null }, { where: { id: authenticatedReq.user.id } }); + await User.update({ api_key: null }, { where: { id: req.user.id } }); capture("maxun-oss-api-key-deleted", { user_id: user.id, From dbe99bafb8930b724e82d8f233751aacfb17bb8f Mon Sep 17 00:00:00 2001 From: Rohit Date: Sat, 22 Feb 2025 14:49:05 +0530 Subject: [PATCH 110/119] feat: revert to auth request --- server/src/routes/auth.ts | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts index 9396a5d4..67eb13dc 100644 --- a/server/src/routes/auth.ts +++ b/server/src/routes/auth.ts @@ -350,8 +350,7 @@ router.get("/google", (req, res) => { router.get( "/google/callback", requireSignIn, - async (req: Request, res) => { - const authenticatedReq = req as AuthenticatedRequest; + async (req: AuthenticatedRequest, res) => { const { code, state } = req.query; try { if (!state) { @@ -377,12 +376,12 @@ router.get( return res.status(400).json({ message: "Email not found" }); } - if (!authenticatedReq.user) { + if (!req.user) { return res.status(401).send({ error: "Unauthorized" }); } // Get the currently authenticated user (from `requireSignIn`) - let user = await User.findOne({ where: { id: authenticatedReq.user.id } }); + let user = await User.findOne({ where: { id: req.user.id } }); if (!user) { return res.status(400).json({ message: "User not found" }); @@ -460,13 +459,12 @@ router.get( router.post( "/gsheets/data", requireSignIn, - async (req: Request, res) => { - const authenticatedReq = req as AuthenticatedRequest; + async (req: AuthenticatedRequest, res) => { const { spreadsheetId, robotId } = req.body; - if (!authenticatedReq.user) { + if (!req.user) { return res.status(401).send({ error: "Unauthorized" }); } - const user = await User.findByPk(authenticatedReq.user.id, { raw: true }); + const user = await User.findByPk(req.user.id, { raw: true }); if (!user) { return res.status(400).json({ message: "User not found" }); @@ -578,14 +576,13 @@ router.post("/gsheets/update", requireSignIn, async (req, res) => { router.post( "/gsheets/remove", requireSignIn, - async (req: Request, res) => { - const authenticatedReq = req as AuthenticatedRequest; + async (req: AuthenticatedRequest, res) => { const { robotId } = req.body; if (!robotId) { return res.status(400).json({ message: "Robot ID is required" }); } - if (!authenticatedReq.user) { + if (!req.user) { return res.status(401).send({ error: "Unauthorized" }); } @@ -607,7 +604,7 @@ router.post( }); capture("maxun-oss-google-sheet-integration-removed", { - user_id: authenticatedReq.user.id, + user_id: req.user.id, robot_id: robotId, deleted_at: new Date().toISOString(), }); From daa889e73499e526d621d695dd84d1012e928d0b Mon Sep 17 00:00:00 2001 From: amhsirak Date: Sat, 22 Feb 2025 21:22:19 +0530 Subject: [PATCH 111/119] feat: revert to auth request --- server/src/routes/auth.ts | 176 ++++++++++---------------------------- 1 file changed, 47 insertions(+), 129 deletions(-) diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts index f77205b4..728cc42c 100644 --- a/server/src/routes/auth.ts +++ b/server/src/routes/auth.ts @@ -140,22 +140,9 @@ router.post("/login", async (req, res) => { } }); -router.get( - "/logout", - requireSignIn, - async (req: Request, res) => { - const authenticatedReq = req as AuthenticatedRequest; +router.get("/logout", async (req, res) => { try { - if (!authenticatedReq.user) { - return res.status(401).json({ - ok: false, - message: "Unauthorized", - code: "unauthorized" - }); - } - res.clearCookie("token"); - return res.status(200).json({ ok: true, message: "Logged out successfully", @@ -176,13 +163,12 @@ router.get( router.get( "/current-user", requireSignIn, - async (req: Request, res) => { - const authenticatedReq = req as AuthenticatedRequest; + async (req: AuthenticatedRequest, res) => { try { - if (!authenticatedReq.user) { + if (!req.user) { return res.status(401).json({ ok: false, error: "Unauthorized" }); } - const user = await User.findByPk(authenticatedReq.user.id, { + const user = await User.findByPk(req.user.id, { attributes: { exclude: ["password"] }, }); if (!user) { @@ -205,7 +191,7 @@ router.get( router.get( "/user/:id", requireSignIn, - async (req: Request, res) => { + async (req: AuthenticatedRequest, res) => { try { const { id } = req.params; if (!id) { @@ -234,60 +220,39 @@ router.get( router.post( "/generate-api-key", requireSignIn, - async (req: Request, res) => { - const authenticatedReq = req as AuthenticatedRequest; + async (req: AuthenticatedRequest, res) => { try { - if (!authenticatedReq.user) { - return res.status(401).json({ - ok: false, - message: "Unauthorized", - code: "unauthorized" - }); + if (!req.user) { + return res.status(401).json({ ok: false, error: "Unauthorized" }); } - - const user = await User.findByPk(authenticatedReq.user.id, { + const user = await User.findByPk(req.user.id, { attributes: { exclude: ["password"] }, }); if (!user) { - return res.status(404).json({ - ok: false, - message: "User not found", - code: "not_found" - }); + return res.status(404).json({ message: "User not found" }); } if (user.api_key) { - return res.status(400).json({ - ok: false, - message: "API key already exists", - code: "key_exists" - }); + return res.status(400).json({ message: "API key already exists" }); } - const apiKey = genAPIKey(); + await user.update({ api_key: apiKey }); - // Capture analytics event capture("maxun-oss-api-key-created", { user_id: user.id, created_at: new Date().toISOString(), }); return res.status(200).json({ - ok: true, message: "API key generated successfully", - api_key: apiKey + api_key: apiKey, }); - } catch (error) { - console.error('API Key generation error:', error); - return res.status(500).json({ - ok: false, - message: "Error generating API key", - code: "server", - error: process.env.NODE_ENV === 'development' ? error : undefined - }); + return res + .status(500) + .json({ message: "Error generating API key", error }); } } ); @@ -295,44 +260,27 @@ router.post( router.get( "/api-key", requireSignIn, - async (req: Request, res) => { - const authenticatedReq = req as AuthenticatedRequest; + async (req: AuthenticatedRequest, res) => { try { - if (!authenticatedReq.user) { - return res.status(401).json({ - ok: false, - message: "Unauthorized", - code: "unauthorized" - }); + if (!req.user) { + return res.status(401).json({ ok: false, error: "Unauthorized" }); } - const user = await User.findByPk(authenticatedReq.user.id, { + const user = await User.findByPk(req.user.id, { raw: true, attributes: ["api_key"], }); if (!user) { - return res.status(404).json({ - ok: false, - message: "User not found", - code: "not_found" - }); + return res.status(404).json({ message: "User not found" }); } return res.status(200).json({ - ok: true, message: "API key fetched successfully", - api_key: user.api_key || null + api_key: user.api_key || null, }); - } catch (error) { - console.error('API Key fetch error:', error); - return res.status(500).json({ - ok: false, - message: "Error fetching API key", - code: "server", - error: process.env.NODE_ENV === 'development' ? error : undefined - }); + return res.status(500).json({ message: "Error fetching API key", error }); } } ); @@ -340,61 +288,34 @@ router.get( router.delete( "/delete-api-key", requireSignIn, - async (req: Request, res) => { - const authenticatedReq = req as AuthenticatedRequest; - try { - if (!authenticatedReq.user) { - return res.status(401).json({ - ok: false, - message: "Unauthorized", - code: "unauthorized" - }); - } + async (req: AuthenticatedRequest, res) => { + if (!req.user) { + return res.status(401).send({ error: "Unauthorized" }); + } - const user = await User.findByPk(authenticatedReq.user.id, { - raw: true, - attributes: ["id", "api_key"] - }); + try { + const user = await User.findByPk(req.user.id, { raw: true }); if (!user) { - return res.status(404).json({ - ok: false, - message: "User not found", - code: "not_found" - }); + return res.status(404).json({ message: "User not found" }); } if (!user.api_key) { - return res.status(404).json({ - ok: false, - message: "API Key not found", - code: "key_not_found" - }); + return res.status(404).json({ message: "API Key not found" }); } - await User.update( - { api_key: null }, - { where: { id: authenticatedReq.user.id } } - ); + await User.update({ api_key: null }, { where: { id: req.user.id } }); capture("maxun-oss-api-key-deleted", { user_id: user.id, deleted_at: new Date().toISOString(), }); - return res.status(200).json({ - ok: true, - message: "API Key deleted successfully" - }); - - } catch (error) { - console.error('API Key deletion error:', error); - return res.status(500).json({ - ok: false, - message: "Error deleting API key", - code: "server", - error: process.env.NODE_ENV === 'development' ? error : undefined - }); + return res.status(200).json({ message: "API Key deleted successfully" }); + } catch (error: any) { + return res + .status(500) + .json({ message: "Error deleting API key", error: error.message }); } } ); @@ -429,8 +350,7 @@ router.get("/google", (req, res) => { router.get( "/google/callback", requireSignIn, - async (req: Request, res) => { - const authenticatedReq = req as AuthenticatedRequest; + async (req: AuthenticatedRequest, res) => { const { code, state } = req.query; try { if (!state) { @@ -456,12 +376,12 @@ router.get( return res.status(400).json({ message: "Email not found" }); } - if (!authenticatedReq.user) { + if (!req.user) { return res.status(401).send({ error: "Unauthorized" }); } // Get the currently authenticated user (from `requireSignIn`) - let user = await User.findOne({ where: { id: authenticatedReq.user.id } }); + let user = await User.findOne({ where: { id: req.user.id } }); if (!user) { return res.status(400).json({ message: "User not found" }); @@ -539,13 +459,12 @@ router.get( router.post( "/gsheets/data", requireSignIn, - async (req: Request, res) => { - const authenticatedReq = req as AuthenticatedRequest; + async (req: AuthenticatedRequest, res) => { const { spreadsheetId, robotId } = req.body; - if (!authenticatedReq.user) { + if (!req.user) { return res.status(401).send({ error: "Unauthorized" }); } - const user = await User.findByPk(authenticatedReq.user.id, { raw: true }); + const user = await User.findByPk(req.user.id, { raw: true }); if (!user) { return res.status(400).json({ message: "User not found" }); @@ -657,14 +576,13 @@ router.post("/gsheets/update", requireSignIn, async (req, res) => { router.post( "/gsheets/remove", requireSignIn, - async (req: Request, res) => { - const authenticatedReq = req as AuthenticatedRequest; + async (req: AuthenticatedRequest, res) => { const { robotId } = req.body; if (!robotId) { return res.status(400).json({ message: "Robot ID is required" }); } - if (!authenticatedReq.user) { + if (!req.user) { return res.status(401).send({ error: "Unauthorized" }); } @@ -686,7 +604,7 @@ router.post( }); capture("maxun-oss-google-sheet-integration-removed", { - user_id: authenticatedReq.user.id, + user_id: req.user.id, robot_id: robotId, deleted_at: new Date().toISOString(), }); @@ -700,4 +618,4 @@ router.post( }); } } -); +); \ No newline at end of file From 18fc3de9f4cd5e7bddbd9652528be585ae2ed921 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Sat, 22 Feb 2025 21:24:32 +0530 Subject: [PATCH 112/119] chore: publish 0.0.12 --- maxun-core/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maxun-core/package.json b/maxun-core/package.json index d0211414..2f04d42a 100644 --- a/maxun-core/package.json +++ b/maxun-core/package.json @@ -1,6 +1,6 @@ { "name": "maxun-core", - "version": "0.0.11", + "version": "0.0.12", "description": "Core package for Maxun, responsible for data extraction", "main": "build/index.js", "typings": "build/index.d.ts", From 09a35da30f8eae656d0b050ca59d4d9239cca178 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Sat, 22 Feb 2025 21:27:59 +0530 Subject: [PATCH 113/119] chore: v0.0.10 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b238a679..99cbcd16 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "maxun", - "version": "0.0.9", + "version": "0.0.10", "author": "Maxun", "license": "AGPL-3.0-or-later", "dependencies": { From 04ba84d8e67e7d064b35b97e8b6ef3d696da362f Mon Sep 17 00:00:00 2001 From: amhsirak Date: Sat, 22 Feb 2025 21:29:01 +0530 Subject: [PATCH 114/119] chore: upgrade core to 0.0.12 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 99cbcd16..0580d2de 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "lodash": "^4.17.21", "loglevel": "^1.8.0", "loglevel-plugin-remote": "^0.6.8", - "maxun-core": "^0.0.11", + "maxun-core": "^0.0.12", "minio": "^8.0.1", "moment-timezone": "^0.5.45", "node-cron": "^3.0.3", From 99b29325aa9eae946ff8ebf840754c9902d5ffe8 Mon Sep 17 00:00:00 2001 From: Rohit Date: Sun, 23 Feb 2025 13:56:19 +0530 Subject: [PATCH 115/119] feat: rm workflow object --- server/src/routes/storage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/routes/storage.ts b/server/src/routes/storage.ts index 5a46dedd..bb3df751 100644 --- a/server/src/routes/storage.ts +++ b/server/src/routes/storage.ts @@ -28,7 +28,7 @@ export const router = Router(); export const processWorkflowActions = async (workflow: any[], checkLimit: boolean = false): Promise => { const processedWorkflow = JSON.parse(JSON.stringify(workflow)); - processedWorkflow.workflow.forEach((pair: any) => { + processedWorkflow.forEach((pair: any) => { pair.what.forEach((action: any) => { // Handle limit validation for scrapeList action if (action.action === 'scrapeList' && checkLimit && Array.isArray(action.args) && action.args.length > 0) { From a5acfe1515624548fffd9918474110a66b729dc7 Mon Sep 17 00:00:00 2001 From: Rohit Date: Mon, 24 Feb 2025 21:45:16 +0530 Subject: [PATCH 116/119] feat: revert api key logic --- src/components/api/ApiKey.tsx | 108 ++++------------------------------ 1 file changed, 13 insertions(+), 95 deletions(-) diff --git a/src/components/api/ApiKey.tsx b/src/components/api/ApiKey.tsx index f4904574..6e2923f9 100644 --- a/src/components/api/ApiKey.tsx +++ b/src/components/api/ApiKey.tsx @@ -43,33 +43,8 @@ const ApiKeyManager = () => { try { const { data } = await axios.get(`${apiUrl}/auth/api-key`); setApiKey(data.api_key); - notify('success', t('apikey.notifications.success.fetch')); } catch (error: any) { - const status = error.response?.status; - let errorKey = 'unknown'; - - switch (status) { - case 401: - errorKey = 'unauthorized'; - break; - case 404: - errorKey = 'not_found'; - break; - case 500: - errorKey = 'server'; - break; - default: - if (error.message?.includes('Network Error')) { - errorKey = 'network'; - } - } - - notify( - 'error', - t(`apikey.notifications.errors.fetch.${errorKey}`, { - error: error.response?.data?.message || error.message - }) - ); + notify('error', t('apikey.notifications.fetch_error', { error: error.message })); } finally { setLoading(false); } @@ -83,36 +58,11 @@ const ApiKeyManager = () => { setLoading(true); try { const { data } = await axios.post(`${apiUrl}/auth/generate-api-key`); - if (data.ok && data.api_key) { - setApiKey(data.api_key); - notify('success', t('apikey.notifications.success.generate')); - } + setApiKey(data.api_key); + + notify('success', t('apikey.notifications.success.generate')); } catch (error: any) { - const status = error.response?.status; - let errorKey = 'unknown'; - - switch (status) { - case 401: - errorKey = 'unauthorized'; - break; - case 403: - errorKey = 'limit_reached'; - break; - case 500: - errorKey = 'server'; - break; - default: - if (error.message?.includes('Network Error')) { - errorKey = 'network'; - } - } - - notify( - 'error', - t(`apikey.notifications.errors.generate.${errorKey}`, { - error: error.response?.data?.message || error.message - }) - ); + notify('error', t('apikey.notifications.generate_error', { error: error.message })); } finally { setLoading(false); } @@ -121,54 +71,22 @@ const ApiKeyManager = () => { const deleteApiKey = async () => { setLoading(true); try { - const response = await axios.delete(`${apiUrl}/auth/delete-api-key`); - if (response.data.ok) { - setApiKey(null); - notify('success', t('apikey.notifications.success.delete')); - } + await axios.delete(`${apiUrl}/auth/delete-api-key`); + setApiKey(null); + notify('success', t('apikey.notifications.delete_success')); } catch (error: any) { - const status = error.response?.status; - let errorKey = 'unknown'; - - switch (status) { - case 401: - errorKey = 'unauthorized'; - break; - case 404: - errorKey = 'not_found'; - break; - case 500: - errorKey = 'server'; - break; - default: - if (error.message?.includes('Network Error')) { - errorKey = 'network'; - } - } - - notify( - 'error', - t(`apikey.notifications.errors.delete.${errorKey}`, { - error: error.response?.data?.message || error.message - }) - ); + notify('error', t('apikey.notifications.delete_error', { error: error.message })); } finally { setLoading(false); } }; - const copyToClipboard = async () => { - if (!apiKey) return; - - try { - await navigator.clipboard.writeText(apiKey); + const copyToClipboard = () => { + if (apiKey) { + navigator.clipboard.writeText(apiKey); setCopySuccess(true); - notify('success', t('apikey.notifications.success.copy')); - - // Reset copy success state after 2 seconds setTimeout(() => setCopySuccess(false), 2000); - } catch (error) { - notify('error', t('apikey.notifications.errors.copy.failed')); + notify('info', t('apikey.notifications.copy_success')); } }; From 7197796d908006a8547912afa4eb59f6ba8eb96d Mon Sep 17 00:00:00 2001 From: Rohit Date: Mon, 24 Feb 2025 21:47:13 +0530 Subject: [PATCH 117/119] feat: revert route logic for api key --- server/src/routes/auth.ts | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts index 728cc42c..1afe45e6 100644 --- a/server/src/routes/auth.ts +++ b/server/src/routes/auth.ts @@ -263,7 +263,11 @@ router.get( async (req: AuthenticatedRequest, res) => { try { if (!req.user) { - return res.status(401).json({ ok: false, error: "Unauthorized" }); + return res.status(401).json({ + ok: false, + error: "Unauthorized", + code: "unauthorized" + }); } const user = await User.findByPk(req.user.id, { @@ -272,15 +276,25 @@ router.get( }); if (!user) { - return res.status(404).json({ message: "User not found" }); + return res.status(404).json({ + ok: false, + error: "User not found", + code: "not_found" + }); } return res.status(200).json({ + ok: true, message: "API key fetched successfully", api_key: user.api_key || null, }); } catch (error) { - return res.status(500).json({ message: "Error fetching API key", error }); + console.error('API Key fetch error:', error); + return res.status(500).json({ + ok: false, + error: "Error fetching API key", + code: "server", + }); } } ); From a57249fbf905d45c7b15ff8ed138666ad0d00e88 Mon Sep 17 00:00:00 2001 From: Rohit Date: Mon, 24 Feb 2025 22:00:07 +0530 Subject: [PATCH 118/119] feat: rectify translation --- src/components/api/ApiKey.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/api/ApiKey.tsx b/src/components/api/ApiKey.tsx index 6e2923f9..f84f7a55 100644 --- a/src/components/api/ApiKey.tsx +++ b/src/components/api/ApiKey.tsx @@ -60,7 +60,7 @@ const ApiKeyManager = () => { const { data } = await axios.post(`${apiUrl}/auth/generate-api-key`); setApiKey(data.api_key); - notify('success', t('apikey.notifications.success.generate')); + notify('success', t('apikey.notifications.generate_success')); } catch (error: any) { notify('error', t('apikey.notifications.generate_error', { error: error.message })); } finally { From c071276b398aeeae4779c3fed2156182d46a0633 Mon Sep 17 00:00:00 2001 From: Rohit Date: Mon, 24 Feb 2025 22:00:40 +0530 Subject: [PATCH 119/119] feat: revert translations for api key --- public/locales/de.json | 41 ++++++----------------------------------- public/locales/en.json | 41 ++++++----------------------------------- public/locales/es.json | 41 ++++++----------------------------------- public/locales/ja.json | 41 ++++++----------------------------------- public/locales/zh.json | 41 ++++++----------------------------------- 5 files changed, 30 insertions(+), 175 deletions(-) diff --git a/public/locales/de.json b/public/locales/de.json index 07cf45bd..7314b677 100644 --- a/public/locales/de.json +++ b/public/locales/de.json @@ -139,41 +139,12 @@ "no_key_message": "Sie haben noch keinen API-Schlüssel generiert.", "generate_button": "API-Schlüssel generieren", "notifications": { - "errors": { - "fetch": { - "network": "Netzwerkfehler beim Abrufen des API-Schlüssels: ${error}", - "unauthorized": "Sie müssen angemeldet sein, um auf den API-Schlüssel zuzugreifen", - "not_found": "API-Schlüssel für Ihr Konto wurde nicht gefunden", - "server": "Serverfehler beim Abrufen des API-Schlüssels. Bitte versuchen Sie es später erneut", - "unknown": "Unbekannter Fehler beim Abrufen des API-Schlüssels: ${error}" - }, - "generate": { - "network": "Netzwerkfehler bei der Generierung des API-Schlüssels: ${error}", - "unauthorized": "Sie müssen angemeldet sein, um einen API-Schlüssel zu generieren", - "key_exists": "Sie haben bereits einen API-Schlüssel. Bitte löschen Sie zuerst den vorhandenen", - "not_found": "Benutzerkonto nicht gefunden", - "server": "Serverfehler bei der Generierung des API-Schlüssels. Bitte versuchen Sie es später erneut", - "unknown": "Unbekannter Fehler bei der Generierung des API-Schlüssels: ${error}" - }, - "delete": { - "network": "Netzwerkfehler beim Löschen des API-Schlüssels: ${error}", - "unauthorized": "Sie müssen angemeldet sein, um den API-Schlüssel zu löschen", - "not_found": "Benutzerkonto nicht gefunden", - "key_not_found": "Kein API-Schlüssel zum Löschen gefunden", - "server": "Serverfehler beim Löschen des API-Schlüssels. Bitte versuchen Sie es später erneut", - "unknown": "Unbekannter Fehler beim Löschen des API-Schlüssels: ${error}" - }, - "copy": { - "failed": "Fehler beim Kopieren des API-Schlüssels in die Zwischenablage", - "no_key": "Kein API-Schlüssel zum Kopieren verfügbar" - } - }, - "success": { - "fetch": "API-Schlüssel erfolgreich abgerufen", - "generate": "Neuer API-Schlüssel erfolgreich generiert", - "delete": "API-Schlüssel erfolgreich gelöscht", - "copy": "API-Schlüssel in die Zwischenablage kopiert" - } + "fetch_error": "API-Schlüssel konnte nicht abgerufen werden - ${error}", + "generate_success": "API-Schlüssel erfolgreich generiert", + "generate_error": "API-Schlüssel konnte nicht generiert werden - ${error}", + "delete_success": "API-Schlüssel erfolgreich gelöscht", + "delete_error": "API-Schlüssel konnte nicht gelöscht werden - ${error}", + "copy_success": "API-Schlüssel erfolgreich kopiert" } }, "action_description": { diff --git a/public/locales/en.json b/public/locales/en.json index e847d832..703ec9c0 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -140,41 +140,12 @@ "no_key_message": "You haven't generated an API key yet.", "generate_button": "Generate API Key", "notifications": { - "errors": { - "fetch": { - "network": "Network error while fetching API key: ${error}", - "unauthorized": "You must be logged in to access API key", - "not_found": "Unable to find API key for your account", - "server": "Server error while fetching API key. Please try again later", - "unknown": "Unknown error occurred while fetching API key: ${error}" - }, - "generate": { - "network": "Network error while generating API key: ${error}", - "unauthorized": "You must be logged in to generate an API key", - "key_exists": "You already have an API key. Please delete the existing one first", - "not_found": "User account not found", - "server": "Server error while generating API key. Please try again later", - "unknown": "Unknown error occurred while generating API key: ${error}" - }, - "delete": { - "network": "Network error while deleting API key: ${error}", - "unauthorized": "You must be logged in to delete API key", - "not_found": "User account not found", - "key_not_found": "No API key found to delete", - "server": "Server error while deleting API key. Please try again later", - "unknown": "Unknown error occurred while deleting API key: ${error}" - }, - "copy": { - "failed": "Failed to copy API key to clipboard", - "no_key": "No API key available to copy" - } - }, - "success": { - "fetch": "API key retrieved successfully", - "generate": "New API key generated successfully", - "delete": "API key deleted successfully", - "copy": "API key copied to clipboard" - } + "fetch_error": "Failed to fetch API Key - ${error}", + "generate_success": "Generated API Key successfully", + "generate_error": "Failed to generate API Key - ${error}", + "delete_success": "API Key deleted successfully", + "delete_error": "Failed to delete API Key - ${error}", + "copy_success": "Copied API Key successfully" } }, "action_description": { diff --git a/public/locales/es.json b/public/locales/es.json index 895ab9bd..ca4edc5d 100644 --- a/public/locales/es.json +++ b/public/locales/es.json @@ -140,41 +140,12 @@ "no_key_message": "Aún no has generado una clave API.", "generate_button": "Generar Clave API", "notifications": { - "errors": { - "fetch": { - "network": "Error de red al obtener la clave API: ${error}", - "unauthorized": "Debes iniciar sesión para acceder a la clave API", - "not_found": "No se pudo encontrar la clave API para tu cuenta", - "server": "Error del servidor al obtener la clave API. Por favor, inténtalo más tarde", - "unknown": "Error desconocido al obtener la clave API: ${error}" - }, - "generate": { - "network": "Error de red al generar la clave API: ${error}", - "unauthorized": "Debes iniciar sesión para generar una clave API", - "key_exists": "Ya tienes una clave API. Por favor, elimina la existente primero", - "not_found": "Cuenta de usuario no encontrada", - "server": "Error del servidor al generar la clave API. Por favor, inténtalo más tarde", - "unknown": "Error desconocido al generar la clave API: ${error}" - }, - "delete": { - "network": "Error de red al eliminar la clave API: ${error}", - "unauthorized": "Debes iniciar sesión para eliminar la clave API", - "not_found": "Cuenta de usuario no encontrada", - "key_not_found": "No se encontró ninguna clave API para eliminar", - "server": "Error del servidor al eliminar la clave API. Por favor, inténtalo más tarde", - "unknown": "Error desconocido al eliminar la clave API: ${error}" - }, - "copy": { - "failed": "Error al copiar la clave API al portapapeles", - "no_key": "No hay clave API disponible para copiar" - } - }, - "success": { - "fetch": "Clave API obtenida exitosamente", - "generate": "Nueva clave API generada exitosamente", - "delete": "Clave API eliminada exitosamente", - "copy": "Clave API copiada al portapapeles" - } + "fetch_error": "Error al obtener la clave API - ${error}", + "generate_success": "Clave API generada con éxito", + "generate_error": "Error al generar la clave API - ${error}", + "delete_success": "Clave API eliminada con éxito", + "delete_error": "Error al eliminar la clave API - ${error}", + "copy_success": "Clave API copiada con éxito" } }, "action_description": { diff --git a/public/locales/ja.json b/public/locales/ja.json index ee18c96c..14e8e9e6 100644 --- a/public/locales/ja.json +++ b/public/locales/ja.json @@ -140,41 +140,12 @@ "no_key_message": "APIキーはまだ生成されていません。", "generate_button": "APIキーを生成", "notifications": { - "errors": { - "fetch": { - "network": "APIキーの取得中にネットワークエラーが発生しました:${error}", - "unauthorized": "APIキーにアクセスするにはログインが必要です", - "not_found": "アカウントのAPIキーが見つかりません", - "server": "APIキーの取得中にサーバーエラーが発生しました。後でもう一度お試しください", - "unknown": "APIキーの取得中に不明なエラーが発生しました:${error}" - }, - "generate": { - "network": "APIキーの生成中にネットワークエラーが発生しました:${error}", - "unauthorized": "APIキーを生成するにはログインが必要です", - "key_exists": "APIキーが既に存在します。既存のキーを先に削除してください", - "not_found": "ユーザーアカウントが見つかりません", - "server": "APIキーの生成中にサーバーエラーが発生しました。後でもう一度お試しください", - "unknown": "APIキーの生成中に不明なエラーが発生しました:${error}" - }, - "delete": { - "network": "APIキーの削除中にネットワークエラーが発生しました:${error}", - "unauthorized": "APIキーを削除するにはログインが必要です", - "not_found": "ユーザーアカウントが見つかりません", - "key_not_found": "削除するAPIキーが見つかりません", - "server": "APIキーの削除中にサーバーエラーが発生しました。後でもう一度お試しください", - "unknown": "APIキーの削除中に不明なエラーが発生しました:${error}" - }, - "copy": { - "failed": "APIキーのクリップボードへのコピーに失敗しました", - "no_key": "コピーできるAPIキーがありません" - } - }, - "success": { - "fetch": "APIキーの取得に成功しました", - "generate": "新しいAPIキーの生成に成功しました", - "delete": "APIキーの削除に成功しました", - "copy": "APIキーをクリップボードにコピーしました" - } + "fetch_error": "APIキーの取得に失敗しました - ${error}", + "generate_success": "APIキーの生成に成功しました", + "generate_error": "APIキーの生成に失敗しました - ${error}", + "delete_success": "APIキーの削除に成功しました", + "delete_error": "APIキーの削除に失敗しました - ${error}", + "copy_success": "APIキーのコピーに成功しました" } }, "action_description": { diff --git a/public/locales/zh.json b/public/locales/zh.json index 5cec8268..5f7a367d 100644 --- a/public/locales/zh.json +++ b/public/locales/zh.json @@ -140,41 +140,12 @@ "no_key_message": "您还未生成API密钥。", "generate_button": "生成API密钥", "notifications": { - "errors": { - "fetch": { - "network": "获取API密钥时发生网络错误:${error}", - "unauthorized": "您必须登录才能访问API密钥", - "not_found": "找不到您账户的API密钥", - "server": "获取API密钥时发生服务器错误。请稍后重试", - "unknown": "获取API密钥时发生未知错误:${error}" - }, - "generate": { - "network": "生成API密钥时发生网络错误:${error}", - "unauthorized": "您必须登录才能生成API密钥", - "key_exists": "您已经有一个API密钥。请先删除现有的密钥", - "not_found": "找不到用户账户", - "server": "生成API密钥时发生服务器错误。请稍后重试", - "unknown": "生成API密钥时发生未知错误:${error}" - }, - "delete": { - "network": "删除API密钥时发生网络错误:${error}", - "unauthorized": "您必须登录才能删除API密钥", - "not_found": "找不到用户账户", - "key_not_found": "找不到要删除的API密钥", - "server": "删除API密钥时发生服务器错误。请稍后重试", - "unknown": "删除API密钥时发生未知错误:${error}" - }, - "copy": { - "failed": "复制API密钥到剪贴板失败", - "no_key": "没有可复制的API密钥" - } - }, - "success": { - "fetch": "成功获取API密钥", - "generate": "成功生成新的API密钥", - "delete": "成功删除API密钥", - "copy": "已将API密钥复制到剪贴板" - } + "fetch_error": "获取API密钥失败 - ${error}", + "generate_success": "成功生成API密钥", + "generate_error": "生成API密钥失败 - ${error}", + "delete_success": "成功删除API密钥", + "delete_error": "删除API密钥失败 - ${error}", + "copy_success": "成功复制API密钥" } }, "action_description": {