feat: recorder revamp ui changes
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
import React, { createContext, useContext, useState } from 'react';
|
||||
import React, { createContext, useContext, useEffect, useRef, useState } from 'react';
|
||||
import { useSocketStore } from "./socket";
|
||||
import { useGlobalInfoStore } from "./globalInfo";
|
||||
|
||||
export interface TextStep {
|
||||
id: number;
|
||||
@@ -8,11 +10,13 @@ export interface TextStep {
|
||||
isShadow?: boolean;
|
||||
selectorObj: SelectorObject;
|
||||
actionId?: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
interface ScreenshotStep {
|
||||
export interface ScreenshotStep {
|
||||
id: number;
|
||||
type: 'screenshot';
|
||||
name?: string;
|
||||
fullPage: boolean;
|
||||
actionId?: string;
|
||||
screenshotData?: string;
|
||||
@@ -21,6 +25,7 @@ interface ScreenshotStep {
|
||||
export interface ListStep {
|
||||
id: number;
|
||||
type: 'list';
|
||||
name?: string;
|
||||
listSelector: string;
|
||||
isShadow?: boolean;
|
||||
fields: { [key: string]: TextStep };
|
||||
@@ -31,112 +36,331 @@ export interface ListStep {
|
||||
};
|
||||
limit?: number;
|
||||
actionId?: string;
|
||||
data?: any[];
|
||||
}
|
||||
|
||||
export type BrowserStep = TextStep | ScreenshotStep | ListStep;
|
||||
|
||||
export interface SelectorObject {
|
||||
selector: string;
|
||||
isShadow?: boolean;
|
||||
tag?: string;
|
||||
attribute?: string;
|
||||
isShadow?: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface BrowserStepsContextType {
|
||||
browserSteps: BrowserStep[];
|
||||
addTextStep: (label: string, data: string, selectorObj: SelectorObject, actionId: string) => void;
|
||||
addListStep: (listSelector: string, fields: { [key: string]: TextStep }, listId: number, actionId: string, pagination?: { type: string; selector: string, isShadow?: boolean }, limit?: number, isShadow?: boolean) => void
|
||||
addTextStep: (
|
||||
label: string,
|
||||
data: string,
|
||||
selectorObj: SelectorObject,
|
||||
actionId: string
|
||||
) => void;
|
||||
addListStep: (
|
||||
listSelector: string,
|
||||
fields: { [key: string]: TextStep },
|
||||
listId: number,
|
||||
actionId: string,
|
||||
pagination?: {
|
||||
type: string;
|
||||
selector: string;
|
||||
isShadow?: boolean;
|
||||
},
|
||||
limit?: number,
|
||||
isShadow?: boolean
|
||||
) => void;
|
||||
addScreenshotStep: (fullPage: boolean, actionId: string) => void;
|
||||
deleteBrowserStep: (id: number) => void;
|
||||
updateBrowserTextStepLabel: (id: number, newLabel: string) => void;
|
||||
updateListTextFieldLabel: (listId: number, fieldKey: string, newLabel: string) => void;
|
||||
updateListTextFieldLabel: (
|
||||
listId: number,
|
||||
fieldKey: string,
|
||||
newLabel: string
|
||||
) => void;
|
||||
updateListStepLimit: (listId: number, limit: number) => void;
|
||||
updateListStepData: (listId: number, extractedData: any[]) => void;
|
||||
updateListStepName: (listId: number, name: string) => void;
|
||||
updateScreenshotStepName: (id: number, name: string) => void;
|
||||
removeListTextField: (listId: number, fieldKey: string) => void;
|
||||
deleteStepsByActionId: (actionId: string) => void;
|
||||
updateScreenshotStepData: (id: number, screenshotData: string) => void;
|
||||
emitActionForStep: (step: BrowserStep) => void;
|
||||
emitForStepId: (actionId: string, nameOverride?: string) => void;
|
||||
}
|
||||
|
||||
const BrowserStepsContext = createContext<BrowserStepsContextType | undefined>(undefined);
|
||||
|
||||
export const BrowserStepsProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const { socket } = useSocketStore();
|
||||
const { currentTextGroupName } = useGlobalInfoStore();
|
||||
const [browserSteps, setBrowserSteps] = useState<BrowserStep[]>([]);
|
||||
const [discardedFields, setDiscardedFields] = useState<Set<string>>(new Set());
|
||||
|
||||
const browserStepsRef = useRef<BrowserStep[]>(browserSteps);
|
||||
useEffect(() => {
|
||||
browserStepsRef.current = browserSteps;
|
||||
}, [browserSteps]);
|
||||
|
||||
const currentTextGroupNameRef = useRef(currentTextGroupName);
|
||||
|
||||
useEffect(() => {
|
||||
currentTextGroupNameRef.current = currentTextGroupName;
|
||||
}, [currentTextGroupName]);
|
||||
|
||||
const getListSettingsObject = (listStep: ListStep) => {
|
||||
const fields: Record<string, {
|
||||
selector: string;
|
||||
tag?: string;
|
||||
[key: string]: any;
|
||||
isShadow?: boolean;
|
||||
}> = {};
|
||||
|
||||
Object.entries(listStep.fields).forEach(([id, field]) => {
|
||||
if (field.selectorObj?.selector) {
|
||||
fields[field.label] = {
|
||||
selector: field.selectorObj.selector,
|
||||
tag: field.selectorObj.tag,
|
||||
attribute: field.selectorObj.attribute,
|
||||
isShadow: field.selectorObj.isShadow
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
listSelector: listStep.listSelector,
|
||||
fields: fields,
|
||||
pagination: {
|
||||
type: listStep.pagination?.type || "",
|
||||
selector: listStep.pagination?.selector,
|
||||
isShadow: listStep.isShadow
|
||||
},
|
||||
limit: listStep.limit,
|
||||
isShadow: listStep.isShadow
|
||||
};
|
||||
};
|
||||
|
||||
const emitActionForStep = (step: BrowserStep) => {
|
||||
if (!socket) return;
|
||||
if (!step.actionId) return;
|
||||
if (!socket.connected) return;
|
||||
|
||||
let action = "";
|
||||
let settings: any = {};
|
||||
|
||||
// Always read the latest steps from the ref to prevent stale data
|
||||
const latestSteps = browserStepsRef.current;
|
||||
|
||||
if (step.type === "list") {
|
||||
action = "scrapeList";
|
||||
const baseSettings = getListSettingsObject(step);
|
||||
settings = {
|
||||
...baseSettings,
|
||||
name: step.name || `List Data ${latestSteps.filter(s => s.type === "list").length}`,
|
||||
};
|
||||
|
||||
} else if (step.type === "text") {
|
||||
action = "scrapeSchema";
|
||||
|
||||
const freshTextSteps = latestSteps.filter(
|
||||
(s): s is TextStep => s.type === "text" && s.actionId === step.actionId
|
||||
);
|
||||
|
||||
// Build schema settings from text steps
|
||||
const fieldSettings: Record<
|
||||
string,
|
||||
{
|
||||
selector: string;
|
||||
tag?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
> = {};
|
||||
|
||||
freshTextSteps.forEach((textStep) => {
|
||||
if (textStep.selectorObj?.selector && textStep.label) {
|
||||
fieldSettings[textStep.label] = {
|
||||
selector: textStep.selectorObj.selector,
|
||||
tag: textStep.selectorObj.tag,
|
||||
attribute: textStep.selectorObj.attribute,
|
||||
isShadow: textStep.selectorObj.isShadow,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
settings = {
|
||||
...fieldSettings,
|
||||
name: currentTextGroupNameRef.current || "Text Data",
|
||||
};
|
||||
|
||||
} else if (step.type === "screenshot") {
|
||||
action = "screenshot";
|
||||
|
||||
const freshScreenshot = latestSteps.find(
|
||||
(s) => s.type === "screenshot" && s.actionId === step.actionId
|
||||
) as ScreenshotStep | undefined;
|
||||
|
||||
settings = {
|
||||
name:
|
||||
step.name ||
|
||||
freshScreenshot?.name ||
|
||||
`Screenshot ${latestSteps.filter((s) => s.type === "screenshot").length}`,
|
||||
type: "png",
|
||||
caret: "hide",
|
||||
scale: "device",
|
||||
timeout: 30000,
|
||||
fullPage: freshScreenshot?.fullPage ?? step.fullPage ?? true,
|
||||
animations: "allow",
|
||||
};
|
||||
}
|
||||
|
||||
socket.emit("action", { action, actionId: step.actionId, settings });
|
||||
};
|
||||
|
||||
const emitForStepId = (actionId: string, nameOverride?: string) => {
|
||||
const step = browserStepsRef.current.find(s => s.actionId === actionId);
|
||||
if (!step) return;
|
||||
|
||||
let enrichedStep = { ...step };
|
||||
|
||||
if (step.type === "text") {
|
||||
enrichedStep = { ...step, name: currentTextGroupNameRef.current };
|
||||
}
|
||||
|
||||
if (step.type === "screenshot") {
|
||||
const freshScreenshot = browserStepsRef.current.find(
|
||||
s => s.type === "screenshot" && s.actionId === actionId
|
||||
) as ScreenshotStep | undefined;
|
||||
|
||||
if (freshScreenshot) {
|
||||
enrichedStep = { ...freshScreenshot };
|
||||
|
||||
if (nameOverride && freshScreenshot.name !== nameOverride) {
|
||||
enrichedStep.name = nameOverride;
|
||||
browserStepsRef.current = browserStepsRef.current.map(s =>
|
||||
s.id === freshScreenshot.id ? { ...s, name: nameOverride } : s
|
||||
);
|
||||
setBrowserSteps(prev =>
|
||||
prev.map(s =>
|
||||
s.id === freshScreenshot.id ? { ...s, name: nameOverride } : s
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (step.type === "list") {
|
||||
const freshList = browserStepsRef.current.find(
|
||||
s => s.type === "list" && s.actionId === actionId
|
||||
) as ListStep | undefined;
|
||||
|
||||
if (freshList) {
|
||||
enrichedStep = { ...freshList };
|
||||
}
|
||||
}
|
||||
|
||||
emitActionForStep(enrichedStep);
|
||||
};
|
||||
|
||||
const addTextStep = (label: string, data: string, selectorObj: SelectorObject, actionId: string) => {
|
||||
setBrowserSteps(prevSteps => [
|
||||
...prevSteps,
|
||||
{ id: Date.now(), type: 'text', label, data, selectorObj, actionId }
|
||||
]);
|
||||
setBrowserSteps((prevSteps) => {
|
||||
const textCount = prevSteps.filter(s => s.type === 'text').length + 1;
|
||||
const generatedLabel = label || `Label ${textCount}`;
|
||||
return [
|
||||
...prevSteps,
|
||||
{
|
||||
id: Date.now(),
|
||||
type: "text",
|
||||
label: generatedLabel,
|
||||
data,
|
||||
selectorObj,
|
||||
actionId,
|
||||
},
|
||||
];
|
||||
});
|
||||
};
|
||||
|
||||
const addListStep = (
|
||||
listSelector: string,
|
||||
newFields: { [key: string]: TextStep },
|
||||
listId: number,
|
||||
actionId: string,
|
||||
pagination?: { type: string; selector: string; isShadow?: boolean },
|
||||
listSelector: string,
|
||||
newFields: { [key: string]: TextStep },
|
||||
listId: number,
|
||||
actionId: string,
|
||||
pagination?: {
|
||||
type: string;
|
||||
selector: string;
|
||||
isShadow?: boolean;
|
||||
},
|
||||
limit?: number,
|
||||
isShadow?: boolean
|
||||
) => {
|
||||
setBrowserSteps(prevSteps => {
|
||||
const existingListStepIndex = prevSteps.findIndex(step => step.type === 'list' && step.id === listId);
|
||||
|
||||
setBrowserSteps((prevSteps) => {
|
||||
const existingListStepIndex = prevSteps.findIndex(
|
||||
(step) => step.type === "list" && step.id === listId
|
||||
);
|
||||
|
||||
if (existingListStepIndex !== -1) {
|
||||
const updatedSteps = [...prevSteps];
|
||||
const existingListStep = updatedSteps[existingListStepIndex] as ListStep;
|
||||
const existingListStep = updatedSteps[
|
||||
existingListStepIndex
|
||||
] as ListStep;
|
||||
|
||||
// Preserve existing labels for fields
|
||||
const mergedFields = Object.entries(newFields).reduce((acc, [key, field]) => {
|
||||
if (!discardedFields.has(`${listId}-${key}`)) {
|
||||
// If field exists, preserve its label
|
||||
if (existingListStep.fields[key]) {
|
||||
acc[key] = {
|
||||
...field,
|
||||
label: existingListStep.fields[key].label,
|
||||
actionId
|
||||
};
|
||||
} else {
|
||||
acc[key] = {
|
||||
...field,
|
||||
actionId
|
||||
};
|
||||
const mergedFields = Object.entries(newFields).reduce(
|
||||
(acc, [key, field]) => {
|
||||
if (!discardedFields.has(`${listId}-${key}`)) {
|
||||
// If field exists, preserve its label
|
||||
if (existingListStep.fields[key]) {
|
||||
acc[key] = {
|
||||
...field,
|
||||
label: existingListStep.fields[key].label,
|
||||
actionId,
|
||||
};
|
||||
} else {
|
||||
acc[key] = {
|
||||
...field,
|
||||
actionId,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return acc;
|
||||
}, {} as { [key: string]: TextStep });
|
||||
return acc;
|
||||
},
|
||||
{} as { [key: string]: TextStep }
|
||||
);
|
||||
|
||||
updatedSteps[existingListStepIndex] = {
|
||||
...existingListStep,
|
||||
listSelector,
|
||||
fields: mergedFields,
|
||||
pagination: pagination || existingListStep.pagination,
|
||||
limit: limit,
|
||||
isShadow: isShadow !== undefined ? isShadow : existingListStep.isShadow,
|
||||
actionId,
|
||||
isShadow: isShadow !== undefined ? isShadow : existingListStep.isShadow
|
||||
};
|
||||
return updatedSteps;
|
||||
} else {
|
||||
const fieldsWithActionId = Object.entries(newFields).reduce((acc, [key, field]) => {
|
||||
acc[key] = {
|
||||
...field,
|
||||
actionId
|
||||
};
|
||||
return acc;
|
||||
}, {} as { [key: string]: TextStep });
|
||||
const fieldsWithActionId = Object.entries(newFields).reduce(
|
||||
(acc, [key, field]) => {
|
||||
acc[key] = {
|
||||
...field,
|
||||
actionId,
|
||||
};
|
||||
return acc;
|
||||
},
|
||||
{} as { [key: string]: TextStep }
|
||||
);
|
||||
|
||||
const listCount = prevSteps.filter(s => s.type === 'list').length + 1;
|
||||
return [
|
||||
...prevSteps,
|
||||
{
|
||||
id: listId,
|
||||
type: 'list',
|
||||
listSelector,
|
||||
fields: fieldsWithActionId,
|
||||
pagination,
|
||||
limit,
|
||||
{
|
||||
id: listId,
|
||||
type: "list",
|
||||
name: `List Data ${listCount}`,
|
||||
listSelector,
|
||||
fields: fieldsWithActionId,
|
||||
pagination,
|
||||
limit,
|
||||
actionId,
|
||||
isShadow
|
||||
}
|
||||
},
|
||||
];
|
||||
}
|
||||
});
|
||||
@@ -165,22 +389,39 @@ export const BrowserStepsProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
);
|
||||
};
|
||||
|
||||
const updateListTextFieldLabel = (listId: number, fieldKey: string, newLabel: string) => {
|
||||
setBrowserSteps(prevSteps =>
|
||||
prevSteps.map(step => {
|
||||
if (step.type === 'list' && step.id === listId) {
|
||||
// Ensure deep copy of the fields object
|
||||
const updateListTextFieldLabel = (
|
||||
listId: number,
|
||||
fieldKey: string,
|
||||
newLabel: string
|
||||
) => {
|
||||
setBrowserSteps((prevSteps) =>
|
||||
prevSteps.map((step) => {
|
||||
if (step.type === "list" && step.id === listId) {
|
||||
const oldLabel = step.fields[fieldKey].label;
|
||||
|
||||
const updatedFields = {
|
||||
...step.fields,
|
||||
[fieldKey]: {
|
||||
...step.fields[fieldKey],
|
||||
label: newLabel
|
||||
}
|
||||
label: newLabel,
|
||||
},
|
||||
};
|
||||
|
||||
const updatedData = step.data?.map((row: any) => {
|
||||
if (row[oldLabel] !== undefined) {
|
||||
const { [oldLabel]: value, ...rest } = row;
|
||||
return {
|
||||
...rest,
|
||||
[newLabel]: value,
|
||||
};
|
||||
}
|
||||
return row;
|
||||
});
|
||||
|
||||
return {
|
||||
...step,
|
||||
fields: updatedFields
|
||||
fields: updatedFields,
|
||||
data: updatedData,
|
||||
};
|
||||
}
|
||||
return step;
|
||||
@@ -194,7 +435,7 @@ export const BrowserStepsProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
if (step.type === 'list' && step.id === listId) {
|
||||
return {
|
||||
...step,
|
||||
data: extractedData // Add the extracted data to the step
|
||||
data: extractedData
|
||||
};
|
||||
}
|
||||
return step;
|
||||
@@ -217,49 +458,83 @@ export const BrowserStepsProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
};
|
||||
|
||||
const updateListStepLimit = (listId: number, limit: number) => {
|
||||
setBrowserSteps(prevSteps =>
|
||||
prevSteps.map(step => {
|
||||
if (step.type === 'list' && step.id === listId) {
|
||||
return {
|
||||
...step,
|
||||
limit: limit
|
||||
};
|
||||
}
|
||||
return step;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const removeListTextField = (listId: number, fieldKey: string) => {
|
||||
setBrowserSteps(prevSteps =>
|
||||
prevSteps.map(step => {
|
||||
if (step.type === 'list' && step.id === listId) {
|
||||
const { [fieldKey]: _, ...remainingFields } = step.fields;
|
||||
return {
|
||||
...step,
|
||||
fields: remainingFields
|
||||
limit: limit
|
||||
};
|
||||
}
|
||||
return step;
|
||||
})
|
||||
);
|
||||
setDiscardedFields(prevDiscarded => new Set(prevDiscarded).add(`${listId}-${fieldKey}`));
|
||||
};
|
||||
|
||||
const updateListStepName = (listId: number, name: string) => {
|
||||
setBrowserSteps((prevSteps) =>
|
||||
prevSteps.map((step) => {
|
||||
if (step.type === "list" && step.id === listId) {
|
||||
return {
|
||||
...step,
|
||||
name: name,
|
||||
};
|
||||
}
|
||||
return step;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const updateScreenshotStepName = (id: number, name: string) => {
|
||||
setBrowserSteps(prevSteps => {
|
||||
const updated = prevSteps.map(step =>
|
||||
step.id === id && step.type === 'screenshot'
|
||||
? { ...step, name }
|
||||
: step
|
||||
);
|
||||
browserStepsRef.current = updated;
|
||||
return updated;
|
||||
});
|
||||
};
|
||||
|
||||
const removeListTextField = (listId: number, fieldKey: string) => {
|
||||
setBrowserSteps((prevSteps) =>
|
||||
prevSteps.map((step) => {
|
||||
if (step.type === "list" && step.id === listId) {
|
||||
const { [fieldKey]: _, ...remainingFields } = step.fields;
|
||||
return {
|
||||
...step,
|
||||
fields: remainingFields,
|
||||
};
|
||||
}
|
||||
return step;
|
||||
})
|
||||
);
|
||||
setDiscardedFields((prevDiscarded) =>
|
||||
new Set(prevDiscarded).add(`${listId}-${fieldKey}`)
|
||||
);
|
||||
};
|
||||
return (
|
||||
<BrowserStepsContext.Provider value={{
|
||||
browserSteps,
|
||||
addTextStep,
|
||||
addListStep,
|
||||
addScreenshotStep,
|
||||
deleteBrowserStep,
|
||||
updateBrowserTextStepLabel,
|
||||
updateListTextFieldLabel,
|
||||
updateListStepLimit,
|
||||
updateListStepData,
|
||||
removeListTextField,
|
||||
deleteStepsByActionId,
|
||||
updateScreenshotStepData,
|
||||
}}>
|
||||
<BrowserStepsContext.Provider
|
||||
value={{
|
||||
browserSteps,
|
||||
addTextStep,
|
||||
addListStep,
|
||||
addScreenshotStep,
|
||||
deleteBrowserStep,
|
||||
updateBrowserTextStepLabel,
|
||||
updateListTextFieldLabel,
|
||||
updateListStepLimit,
|
||||
updateListStepData,
|
||||
updateListStepName,
|
||||
updateScreenshotStepName,
|
||||
removeListTextField,
|
||||
deleteStepsByActionId,
|
||||
updateScreenshotStepData,
|
||||
emitActionForStep,
|
||||
emitForStepId
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</BrowserStepsContext.Provider>
|
||||
);
|
||||
|
||||
@@ -139,6 +139,8 @@ interface GlobalInfo {
|
||||
setCurrentListActionId: (actionId: string) => void;
|
||||
currentScreenshotActionId: string;
|
||||
setCurrentScreenshotActionId: (actionId: string) => void;
|
||||
currentTextGroupName: string;
|
||||
setCurrentTextGroupName: (name: string) => void;
|
||||
isDOMMode: boolean;
|
||||
setIsDOMMode: (isDOMMode: boolean) => void;
|
||||
currentSnapshot: ProcessedSnapshot | null;
|
||||
@@ -173,6 +175,7 @@ class GlobalInfoStore implements Partial<GlobalInfo> {
|
||||
currentTextActionId = '';
|
||||
currentListActionId = '';
|
||||
currentScreenshotActionId = '';
|
||||
currentTextGroupName = 'Text Data';
|
||||
isDOMMode = false;
|
||||
currentSnapshot = null;
|
||||
};
|
||||
@@ -282,6 +285,7 @@ export const GlobalInfoProvider = ({ children }: { children: JSX.Element }) => {
|
||||
const [currentTextActionId, setCurrentTextActionId] = useState<string>('');
|
||||
const [currentListActionId, setCurrentListActionId] = useState<string>('');
|
||||
const [currentScreenshotActionId, setCurrentScreenshotActionId] = useState<string>('');
|
||||
const [currentTextGroupName, setCurrentTextGroupName] = useState<string>('Text Data');
|
||||
const [isDOMMode, setIsDOMMode] = useState<boolean>(globalInfoStore.isDOMMode);
|
||||
const [currentSnapshot, setCurrentSnapshot] = useState<ProcessedSnapshot | null>(globalInfoStore.currentSnapshot);
|
||||
|
||||
@@ -363,6 +367,8 @@ export const GlobalInfoProvider = ({ children }: { children: JSX.Element }) => {
|
||||
setCurrentListActionId,
|
||||
currentScreenshotActionId,
|
||||
setCurrentScreenshotActionId,
|
||||
currentTextGroupName,
|
||||
setCurrentTextGroupName,
|
||||
isDOMMode,
|
||||
setIsDOMMode,
|
||||
currentSnapshot,
|
||||
|
||||
Reference in New Issue
Block a user