feat(snippets): add snippets entity, crud, and auto-browser-per-run

- add Snippet entity with name, description, code; full CRUD backend
- add snippets pages (list, create, edit, detail) and nav entry
- add runSnippet helper in code-executor using new Function with args array
- add result param to execute() so validateCode can access exec output
- remove sessionName from steps; each run now spawns its own fresh browser
- fix waitForURL race by polling localStorage for token instead
This commit is contained in:
2026-04-10 00:22:51 +03:00
parent 1efbbb38a3
commit 1164289173
26 changed files with 876 additions and 89 deletions
@@ -18,10 +18,8 @@ export function EditStepPage() {
const [order, setOrder] = useState('');
const [title, setTitle] = useState('');
const [type, setType] = useState<StepType>('exec');
const [sessionName, setSessionName] = useState('');
const [execCode, setExecCode] = useState('');
const [validateCode, setValidateCode] = useState('');
const [sessionError, setSessionError] = useState('');
const [orderError, setOrderError] = useState('');
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
@@ -36,7 +34,6 @@ export function EditStepPage() {
setOrder(String(st.order));
setTitle(st.title ?? '');
setType(st.type);
setSessionName(st.sessionName);
setExecCode(st.execCode ?? '');
setValidateCode(st.validateCode ?? '');
})
@@ -46,10 +43,6 @@ export function EditStepPage() {
const validate = (): boolean => {
let valid = true;
if (!sessionName.trim()) {
setSessionError(t('steps.form_session_required'));
valid = false;
}
if (isNaN(Number(order)) || Number(order) < 0) {
setOrderError(t('steps.form_order_invalid'));
valid = false;
@@ -67,7 +60,6 @@ export function EditStepPage() {
order: Number(order),
title: title.trim() || undefined,
type,
sessionName: sessionName.trim(),
execCode: execCode.trim() || undefined,
validateCode: validateCode.trim() || undefined,
});
@@ -124,17 +116,6 @@ export function EditStepPage() {
onChange={(e) => setType(e.target.value as StepType)}
options={STEP_TYPES.map((v) => ({ value: v, label: v }))}
/>
<Input
label={t('steps.form_session')}
placeholder={t('steps.form_session_placeholder')}
value={sessionName}
onChange={(e) => {
setSessionName(e.target.value);
setSessionError('');
}}
error={sessionError || undefined}
required
/>
<div className={styles.formField}>
<label className={styles.fieldLabel}>{t('steps.form_exec_code')}</label>
<textarea