mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-07-15 23:35:17 +03:00
Merge branch 'dev'
This commit is contained in:
@@ -118,12 +118,14 @@ Switch to an already-open tab/window (fuzzy match).
|
|||||||
|
|
||||||
### Reading form state
|
### Reading form state
|
||||||
|
|
||||||
#### `getFormState()` → `{ fields, buttons, tabs, table, filters, reportSettings? }`
|
#### `getFormState()` → `{ fields, buttons, tabs, table, tables, filters, reportSettings? }`
|
||||||
Returns current form structure. This is the primary way to understand what's on screen.
|
Returns current form structure. This is the primary way to understand what's on screen.
|
||||||
|
|
||||||
**fields** — each field has: `name`, `value`, `label?`, `actions?` (select, clear, open), `required?` (true for unfilled mandatory fields)
|
**fields** — each field has: `name`, `value`, `label?`, `actions?` (select, clear, open), `required?` (true for unfilled mandatory fields)
|
||||||
|
|
||||||
**table** — summary only: `{ name, columns, rowCount }`. Use `readTable()` for actual data.
|
**tables** — array of all visible grids: `[{ name, columns, rowCount, label? }]`. `label` is the visual group title shown on screen (e.g. "Входящие"), absent when grid has no visible title. Use `readTable()` for actual data.
|
||||||
|
|
||||||
|
**table** — backward-compatible alias for the first grid: `{ present, columns, rowCount }`.
|
||||||
|
|
||||||
**reportSettings** — for DCS reports: human-readable filter settings instead of raw technical names:
|
**reportSettings** — for DCS reports: human-readable filter settings instead of raw technical names:
|
||||||
```js
|
```js
|
||||||
@@ -140,13 +142,14 @@ const form = await getFormState();
|
|||||||
|
|
||||||
### Reading data
|
### Reading data
|
||||||
|
|
||||||
#### `readTable({ maxRows?, offset? })` → `{ columns, rows, total, shown, offset }`
|
#### `readTable({ maxRows?, offset?, table? })` → `{ columns, rows, total, shown, offset }`
|
||||||
Read actual grid data with pagination. Each row is `{ columnName: value }`.
|
Read actual grid data with pagination. Each row is `{ columnName: value }`.
|
||||||
|
|
||||||
| Option | Default | Description |
|
| Option | Default | Description |
|
||||||
|--------|---------|-------------|
|
|--------|---------|-------------|
|
||||||
| `maxRows` | 20 | Max rows to return per call |
|
| `maxRows` | 20 | Max rows to return per call |
|
||||||
| `offset` | 0 | Skip first N rows |
|
| `offset` | 0 | Skip first N rows |
|
||||||
|
| `table` | — | Grid name from `tables[]` (for multi-grid forms) |
|
||||||
|
|
||||||
Special row fields:
|
Special row fields:
|
||||||
- `_kind: 'group'` — hierarchical group row
|
- `_kind: 'group'` — hierarchical group row
|
||||||
@@ -190,9 +193,13 @@ Sections + all open tabs.
|
|||||||
|
|
||||||
### Actions
|
### Actions
|
||||||
|
|
||||||
#### `clickElement(text, { dblclick? })` → form state
|
#### `clickElement(text, { dblclick?, table? })` → form state
|
||||||
Click button, hyperlink, tab, or grid row (fuzzy match).
|
Click button, hyperlink, tab, or grid row (fuzzy match).
|
||||||
|
|
||||||
|
- `table` — scope button search to a specific grid's command panel (by name from `tables[]`):
|
||||||
|
```js
|
||||||
|
await clickElement('Добавить', { table: 'Исходящие' }); // clicks "Добавить" near "Исходящие" grid
|
||||||
|
```
|
||||||
- Single click selects a row in a list. **Double-click opens** the item:
|
- Single click selects a row in a list. **Double-click opens** the item:
|
||||||
```js
|
```js
|
||||||
await clickElement('0000-000227', { dblclick: true }); // opens document
|
await clickElement('0000-000227', { dblclick: true }); // opens document
|
||||||
@@ -250,6 +257,13 @@ Also supports DCS labels — auto-enables the paired checkbox.
|
|||||||
#### `fillTableRow(fields, opts)` → form state
|
#### `fillTableRow(fields, opts)` → form state
|
||||||
Fill table row cells via Tab navigation. Value is a plain string or `{ value, type }` for composite-type cells.
|
Fill table row cells via Tab navigation. Value is a plain string or `{ value, type }` for composite-type cells.
|
||||||
|
|
||||||
|
| Option | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `tab` | Switch to tab before filling |
|
||||||
|
| `add` | Add new row before filling |
|
||||||
|
| `row` | Edit existing row by 0-based index |
|
||||||
|
| `table` | Grid name from `tables[]` (for multi-grid forms) |
|
||||||
|
|
||||||
```js
|
```js
|
||||||
// Add new row:
|
// Add new row:
|
||||||
await fillTableRow(
|
await fillTableRow(
|
||||||
@@ -261,6 +275,11 @@ await fillTableRow(
|
|||||||
{ 'Количество': '20' },
|
{ 'Количество': '20' },
|
||||||
{ tab: 'Товары', row: 0 }
|
{ tab: 'Товары', row: 0 }
|
||||||
);
|
);
|
||||||
|
// Multi-grid form — add row to specific table:
|
||||||
|
await fillTableRow(
|
||||||
|
{ 'Объект': 'БДДС' },
|
||||||
|
{ table: 'Исходящие', add: true }
|
||||||
|
);
|
||||||
// Composite-type cell (e.g. SubConto accepting multiple types):
|
// Composite-type cell (e.g. SubConto accepting multiple types):
|
||||||
await fillTableRow(
|
await fillTableRow(
|
||||||
{ 'СубконтоКт1': { value: 'Голованов', type: 'Физическое лицо' } },
|
{ 'СубконтоКт1': { value: 'Голованов', type: 'Физическое лицо' } },
|
||||||
@@ -272,8 +291,8 @@ await fillTableRow(
|
|||||||
- Fuzzy cell match: "Количество" matches "ТоварыКоличество"
|
- Fuzzy cell match: "Количество" matches "ТоварыКоличество"
|
||||||
- Reference cells auto-detected by autocomplete popup
|
- Reference cells auto-detected by autocomplete popup
|
||||||
|
|
||||||
#### `deleteTableRow(row, { tab? })` → form state
|
#### `deleteTableRow(row, { tab?, table? })` → form state
|
||||||
Delete row by 0-based index.
|
Delete row by 0-based index. `table` targets a specific grid on multi-grid forms.
|
||||||
|
|
||||||
#### `closeForm({ save? })` → form state
|
#### `closeForm({ save? })` → form state
|
||||||
Close the current form via Escape.
|
Close the current form via Escape.
|
||||||
@@ -359,6 +378,37 @@ console.log('Title:', report.title);
|
|||||||
console.log('Data rows:', report.data?.length);
|
console.log('Data rows:', report.data?.length);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Work with multi-grid forms
|
||||||
|
|
||||||
|
Some forms have multiple grids (e.g. "Входящие" and "Исходящие" tables on a single form). Without `table`, buttons like "Добавить" hit the first match and `readTable` reads the first grid — which may not be the one you need.
|
||||||
|
|
||||||
|
**Step 1 — discover tables** via `getFormState()`:
|
||||||
|
```js
|
||||||
|
const form = await getFormState();
|
||||||
|
// form.tables = [
|
||||||
|
// { name: "ДеревоБизнесПроцессов", columns: ["Полный код", "Бизнес-процесс"], rowCount: 21 },
|
||||||
|
// { name: "Входящие", label: "Входящие", columns: ["Объект", "Бизнес-процесс источник", ...], rowCount: 1 },
|
||||||
|
// { name: "Исходящие", label: "Исходящие", columns: ["Объект", "Бизнес-процесс приемник", ...], rowCount: 1 }
|
||||||
|
// ]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2 — use `table` name** in any grid operation:
|
||||||
|
```js
|
||||||
|
// Read specific table
|
||||||
|
const t = await readTable({ table: 'Исходящие' });
|
||||||
|
|
||||||
|
// Add row — fillTableRow with add:true already clicks the right "Добавить" button
|
||||||
|
await fillTableRow({ 'Объект': 'БДДС' }, { table: 'Исходящие', add: true });
|
||||||
|
|
||||||
|
// Or click buttons separately
|
||||||
|
await clickElement('Добавить', { table: 'Входящие' });
|
||||||
|
|
||||||
|
// Delete from specific table
|
||||||
|
await deleteTableRow(0, { table: 'Исходящие' });
|
||||||
|
```
|
||||||
|
|
||||||
|
Table matching accepts both technical name (`tables[].name`) and visual label (`tables[].label`). Label is the group title shown on screen — useful when working from screenshots. Name match takes priority over label match.
|
||||||
|
|
||||||
### Keyboard shortcuts (via `page.keyboard.press`)
|
### Keyboard shortcuts (via `page.keyboard.press`)
|
||||||
|
|
||||||
| Key | Context | Action |
|
| Key | Context | Action |
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
findClickTargetScript, findFieldButtonScript, readSubmenuScript,
|
findClickTargetScript, findFieldButtonScript, readSubmenuScript,
|
||||||
resolveFieldsScript, getFormStateScript,
|
resolveFieldsScript, getFormStateScript,
|
||||||
detectFormScript, readTableScript, checkErrorsScript,
|
detectFormScript, readTableScript, checkErrorsScript,
|
||||||
switchTabScript
|
switchTabScript, resolveGridScript
|
||||||
} from './dom.mjs';
|
} from './dom.mjs';
|
||||||
|
|
||||||
let browser = null;
|
let browser = null;
|
||||||
@@ -548,11 +548,17 @@ export async function getFormState() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Read structured table data with pagination. Returns columns, rows, total count. */
|
/** Read structured table data with pagination. Returns columns, rows, total count. */
|
||||||
export async function readTable({ maxRows = 20, offset = 0 } = {}) {
|
export async function readTable({ maxRows = 20, offset = 0, table } = {}) {
|
||||||
ensureConnected();
|
ensureConnected();
|
||||||
const formNum = await page.evaluate(detectFormScript());
|
const formNum = await page.evaluate(detectFormScript());
|
||||||
if (formNum === null) throw new Error('readTable: no form found');
|
if (formNum === null) throw new Error('readTable: no form found');
|
||||||
return await page.evaluate(readTableScript(formNum, { maxRows, offset }));
|
let gridSelector;
|
||||||
|
if (table) {
|
||||||
|
const resolved = await page.evaluate(resolveGridScript(formNum, table));
|
||||||
|
if (resolved.error) throw new Error(`readTable: ${resolved.message || resolved.error}. Available: ${resolved.available?.map(a => a.name).join(', ') || 'none'}`);
|
||||||
|
gridSelector = resolved.gridSelector;
|
||||||
|
}
|
||||||
|
return await page.evaluate(readTableScript(formNum, { maxRows, offset, gridSelector }));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -739,72 +745,50 @@ export async function readSpreadsheet() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pick a value from an opened selection form: filter + dblclick matching row.
|
* Scan visible grid rows for a text match (exact → startsWith → includes).
|
||||||
*
|
* Returns center coords of the matched row, or null if not found.
|
||||||
* Strategy:
|
* When searchLower is empty, returns coords of the first row (fallback).
|
||||||
* - string: simple search via filterList → dblclick first row
|
|
||||||
* - object: advanced search (Alt+F) for each field sequentially
|
|
||||||
* (searches by specific column, efficient on large tables) → dblclick positioned row
|
|
||||||
*
|
|
||||||
* @param {number} selFormNum - selection form number
|
|
||||||
* @param {string} fieldName - field being filled (for error messages)
|
|
||||||
* @param {string|Object} search - string for simple search, or { field: value } for per-field search
|
|
||||||
* @param {number} origFormNum - original form number (to verify we returned)
|
|
||||||
* @returns {{ field, ok, method }} or {{ field, error, message }}
|
|
||||||
*/
|
*/
|
||||||
async function pickFromSelectionForm(selFormNum, fieldName, search, origFormNum) {
|
async function scanGridRows(formNum, searchLower) {
|
||||||
// 1. Apply filters based on search type
|
return page.evaluate(`(() => {
|
||||||
if (typeof search === 'string') {
|
const p = 'form${formNum}_';
|
||||||
// Simple text search
|
|
||||||
if (search) {
|
|
||||||
try {
|
|
||||||
await filterList(search);
|
|
||||||
} catch (e) {
|
|
||||||
await page.keyboard.press('Escape');
|
|
||||||
await waitForStable();
|
|
||||||
return { field: fieldName, error: 'filter_failed', message: e.message };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (search && typeof search === 'object') {
|
|
||||||
// Per-field advanced search (Alt+F) for each entry — searches by specific column,
|
|
||||||
// more efficient than simple search on large tables (no full-text scan across all columns).
|
|
||||||
const entries = Object.entries(search);
|
|
||||||
for (const [fld, val] of entries) {
|
|
||||||
try {
|
|
||||||
await filterList(String(val), { field: fld });
|
|
||||||
} catch (e) {
|
|
||||||
// Advanced search failed — fall through and try with what we have
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Find the target row — currently selected (positioned by advanced search) or first
|
|
||||||
const rowTarget = await page.evaluate(`(() => {
|
|
||||||
const p = 'form${selFormNum}_';
|
|
||||||
const grid = document.querySelector('[id^="' + p + '"].grid, [id^="' + p + '"] .grid');
|
const grid = document.querySelector('[id^="' + p + '"].grid, [id^="' + p + '"] .grid');
|
||||||
if (!grid) return null;
|
if (!grid) return null;
|
||||||
const body = grid.querySelector('.gridBody');
|
const body = grid.querySelector('.gridBody');
|
||||||
if (!body) return null;
|
if (!body) return null;
|
||||||
const lines = [...body.querySelectorAll('.gridLine')];
|
const lines = [...body.querySelectorAll('.gridLine')];
|
||||||
if (!lines.length) return { rowCount: 0 };
|
if (!lines.length) return { rowCount: 0 };
|
||||||
// Prefer selected/active row (positioned by advanced search), fall back to first
|
const searchLower = ${JSON.stringify(searchLower || '')};
|
||||||
const sel = lines.find(l => l.classList.contains('select') || l.classList.contains('active')) || lines[0];
|
let sel = null;
|
||||||
|
if (searchLower) {
|
||||||
|
const norm = s => (s || '').replace(/\\u00a0/g, ' ').trim().toLowerCase().replace(/ё/gi, 'е');
|
||||||
|
const rowData = lines.map(l => ({ el: l, text: norm(l.innerText) }));
|
||||||
|
sel = rowData.find(r => r.text === searchLower)?.el
|
||||||
|
|| rowData.find(r => r.text.startsWith(searchLower))?.el
|
||||||
|
|| rowData.find(r => r.text.includes(searchLower))?.el;
|
||||||
|
} else {
|
||||||
|
sel = lines[0]; // empty search → first row
|
||||||
|
}
|
||||||
|
if (!sel) return null;
|
||||||
const r = sel.getBoundingClientRect();
|
const r = sel.getBoundingClientRect();
|
||||||
return { rowCount: lines.length, matched: true,
|
return { rowCount: lines.length, x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) };
|
||||||
x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) };
|
|
||||||
})()`);
|
})()`);
|
||||||
|
}
|
||||||
|
|
||||||
if (!rowTarget?.matched) {
|
/**
|
||||||
await page.keyboard.press('Escape');
|
* Select a row in a selection form via click + Enter, verify it closed.
|
||||||
await waitForStable();
|
* Uses click + Enter instead of dblclick because dblclick toggles
|
||||||
const searchDesc = typeof search === 'string' ? '"' + search + '"' : JSON.stringify(search);
|
* expand/collapse in tree-style selection forms.
|
||||||
return { field: fieldName, error: 'not_found',
|
* Returns { field, ok: true, method: 'form' } on success,
|
||||||
message: 'No matches in selection form for ' + searchDesc +
|
* or { field, ok: false, reason: 'still_open' } if the item couldn't be selected (e.g. group row).
|
||||||
(rowTarget?.rowCount === 0 ? ' (grid empty)' : '') };
|
*/
|
||||||
}
|
async function dblclickAndVerify(coords, selFormNum, fieldName) {
|
||||||
|
// Click to highlight the row, then Enter to confirm selection.
|
||||||
// 3. Dblclick the target row
|
// This works for both flat grids and tree forms (dblclick would
|
||||||
await page.mouse.dblclick(rowTarget.x, rowTarget.y);
|
// toggle expand/collapse on tree group rows).
|
||||||
|
await page.mouse.click(coords.x, coords.y);
|
||||||
|
await page.waitForTimeout(200);
|
||||||
|
await page.keyboard.press('Enter');
|
||||||
await waitForStable(selFormNum);
|
await waitForStable(selFormNum);
|
||||||
|
|
||||||
// Verify selection form closed
|
// Verify selection form closed
|
||||||
@@ -813,19 +797,9 @@ async function pickFromSelectionForm(selFormNum, fieldName, search, origFormNum)
|
|||||||
return [...document.querySelectorAll('[id^="' + p + '"]')].some(el => el.offsetWidth > 0);
|
return [...document.querySelectorAll('[id^="' + p + '"]')].some(el => el.offsetWidth > 0);
|
||||||
})()`);
|
})()`);
|
||||||
if (stillOpen) {
|
if (stillOpen) {
|
||||||
// Dblclick may have opened a folder — try Enter to select current row
|
// Enter didn't select — item is likely a non-selectable group.
|
||||||
await page.keyboard.press('Enter');
|
// Don't Escape here — let the caller decide (may want to try another row).
|
||||||
await waitForStable(selFormNum);
|
return { field: fieldName, ok: false, reason: 'still_open' };
|
||||||
|
|
||||||
// Still open? Close and report
|
|
||||||
const stillOpen2 = await page.evaluate(`(() => {
|
|
||||||
const p = 'form${selFormNum}_';
|
|
||||||
return [...document.querySelectorAll('[id^="' + p + '"]')].some(el => el.offsetWidth > 0);
|
|
||||||
})()`);
|
|
||||||
if (stillOpen2) {
|
|
||||||
await page.keyboard.press('Escape');
|
|
||||||
await waitForStable();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for 1C error modals after selection
|
// Check for 1C error modals after selection
|
||||||
@@ -839,6 +813,187 @@ async function pickFromSelectionForm(selFormNum, fieldName, search, origFormNum)
|
|||||||
return { field: fieldName, ok: true, method: 'form' };
|
return { field: fieldName, ok: true, method: 'form' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inline advanced search on a selection form via Alt+F.
|
||||||
|
* Does NOT click any column — FieldSelector auto-populates with main representation.
|
||||||
|
* Switches to "по части строки" (CompareType#1) to avoid composite type issues.
|
||||||
|
* Does not throw — returns silently on failure.
|
||||||
|
*/
|
||||||
|
async function advancedSearchInline(formNum, text) {
|
||||||
|
try {
|
||||||
|
// 1. Open advanced search via Alt+F
|
||||||
|
await page.keyboard.press('Alt+f');
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
|
const dialogForm = await page.evaluate(detectFormScript());
|
||||||
|
if (dialogForm === formNum || dialogForm === null) return; // Alt+F didn't open dialog
|
||||||
|
|
||||||
|
// 2. Switch to "по части строки" (CompareType#1)
|
||||||
|
const radioClicked = await page.evaluate(`(() => {
|
||||||
|
const p = 'form${dialogForm}_';
|
||||||
|
const el = document.getElementById(p + 'CompareType#1#radio');
|
||||||
|
if (!el || el.offsetWidth === 0) return false;
|
||||||
|
if (el.classList.contains('select')) return true; // already selected
|
||||||
|
const r = el.getBoundingClientRect();
|
||||||
|
return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) };
|
||||||
|
})()`);
|
||||||
|
if (radioClicked && typeof radioClicked === 'object') {
|
||||||
|
await page.mouse.click(radioClicked.x, radioClicked.y);
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Fill Pattern field via clipboard paste
|
||||||
|
const patternId = await page.evaluate(`(() => {
|
||||||
|
const p = 'form${dialogForm}_';
|
||||||
|
const el = [...document.querySelectorAll('input.editInput[id^="' + p + '"]')]
|
||||||
|
.find(el => el.offsetWidth > 0 && /Pattern/i.test(el.id));
|
||||||
|
return el ? el.id : null;
|
||||||
|
})()`);
|
||||||
|
if (!patternId) {
|
||||||
|
await page.keyboard.press('Escape');
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await page.click(`[id="${patternId}"]`);
|
||||||
|
await page.waitForTimeout(200);
|
||||||
|
await page.keyboard.press('Control+A');
|
||||||
|
await page.evaluate(`navigator.clipboard.writeText(${JSON.stringify(String(text))})`);
|
||||||
|
await page.keyboard.press('Control+V');
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
|
||||||
|
// 4. Click "Найти"
|
||||||
|
const findBtn = await page.evaluate(`(() => {
|
||||||
|
const btns = [...document.querySelectorAll('a.press')].filter(el => el.offsetWidth > 0);
|
||||||
|
const btn = btns.find(el => el.innerText?.trim() === 'Найти');
|
||||||
|
if (!btn) return null;
|
||||||
|
const r = btn.getBoundingClientRect();
|
||||||
|
return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) };
|
||||||
|
})()`);
|
||||||
|
if (findBtn) {
|
||||||
|
await page.mouse.click(findBtn.x, findBtn.y);
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Close advanced search dialog
|
||||||
|
for (let attempt = 0; attempt < 3; attempt++) {
|
||||||
|
const dialogVisible = await page.evaluate(`(() => {
|
||||||
|
const p = 'form${dialogForm}_';
|
||||||
|
return [...document.querySelectorAll('[id^="' + p + '"]')].some(el => el.offsetWidth > 0);
|
||||||
|
})()`);
|
||||||
|
if (!dialogVisible) break;
|
||||||
|
await page.keyboard.press('Escape');
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
}
|
||||||
|
await waitForStable(formNum);
|
||||||
|
} catch { /* silently fail — caller will re-scan and handle not_found */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pick a value from an opened selection form.
|
||||||
|
*
|
||||||
|
* Strategy (escalating):
|
||||||
|
* 1. Scan visible rows for text match (exact → startsWith → includes)
|
||||||
|
* 2. Simple search (search input + Enter) → re-scan
|
||||||
|
* 3. Advanced search (Alt+F, "по части строки") → re-scan
|
||||||
|
* 4. Not found → Escape → error
|
||||||
|
*
|
||||||
|
* For object search {field: value}: steps 1, then filterList(val, {field}) per entry, then re-scan.
|
||||||
|
* For empty search: pick first visible row.
|
||||||
|
*
|
||||||
|
* @param {number} selFormNum - selection form number
|
||||||
|
* @param {string} fieldName - field being filled (for error messages)
|
||||||
|
* @param {string|Object} search - string for simple search, or { field: value } for per-field search
|
||||||
|
* @param {number} origFormNum - original form number (to verify we returned)
|
||||||
|
* @returns {{ field, ok, method }} or {{ field, error, message }}
|
||||||
|
*/
|
||||||
|
async function pickFromSelectionForm(selFormNum, fieldName, search, origFormNum) {
|
||||||
|
const searchText = typeof search === 'string'
|
||||||
|
? search : (search ? Object.values(search).join(' ') : '');
|
||||||
|
const searchLower = normYo((searchText || '').toLowerCase());
|
||||||
|
|
||||||
|
// Helper: try to select a row; returns result if ok, null if item wasn't selectable (group).
|
||||||
|
let hadUnselectableMatch = false;
|
||||||
|
async function trySelect(row) {
|
||||||
|
const r = await dblclickAndVerify(row, selFormNum, fieldName);
|
||||||
|
if (r.ok) return r;
|
||||||
|
hadUnselectableMatch = true; // found match but couldn't select (group row)
|
||||||
|
return null; // form still open, try next step
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 1: Scan visible rows (no filtering)
|
||||||
|
if (searchLower) {
|
||||||
|
const row = await scanGridRows(selFormNum, searchLower);
|
||||||
|
if (row?.x) {
|
||||||
|
const r = await trySelect(row);
|
||||||
|
if (r) return r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Simple search via search input (directly on the known form, avoids filterList form-detection)
|
||||||
|
if (typeof search === 'string' && searchLower) {
|
||||||
|
const searchInputId = await page.evaluate(`(() => {
|
||||||
|
const p = 'form${selFormNum}_';
|
||||||
|
const el = [...document.querySelectorAll('input.editInput[id^="' + p + '"]')]
|
||||||
|
.find(el => el.offsetWidth > 0 && /Строк[аи]Поиска|SearchString/i.test(el.id));
|
||||||
|
return el ? el.id : null;
|
||||||
|
})()`);
|
||||||
|
if (searchInputId) {
|
||||||
|
try {
|
||||||
|
await page.click(`[id="${searchInputId}"]`);
|
||||||
|
await page.waitForTimeout(200);
|
||||||
|
await page.keyboard.press('Control+A');
|
||||||
|
await page.evaluate(`navigator.clipboard.writeText(${JSON.stringify(String(searchText))})`);
|
||||||
|
await page.keyboard.press('Control+V');
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
await page.keyboard.press('Enter');
|
||||||
|
await waitForStable(selFormNum);
|
||||||
|
} catch { /* proceed to advanced search */ }
|
||||||
|
const row = await scanGridRows(selFormNum, searchLower);
|
||||||
|
if (row?.x) {
|
||||||
|
const r = await trySelect(row);
|
||||||
|
if (r) return r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: Advanced search
|
||||||
|
if (typeof search === 'object' && search) {
|
||||||
|
// Per-field advanced search via filterList(val, {field})
|
||||||
|
for (const [fld, val] of Object.entries(search)) {
|
||||||
|
try { await filterList(String(val), { field: fld }); } catch { /* proceed */ }
|
||||||
|
}
|
||||||
|
} else if (searchLower) {
|
||||||
|
// Inline advanced search (Alt+F, "по части строки")
|
||||||
|
await advancedSearchInline(selFormNum, searchText);
|
||||||
|
}
|
||||||
|
if (searchLower) {
|
||||||
|
const row = await scanGridRows(selFormNum, searchLower);
|
||||||
|
if (row?.x) {
|
||||||
|
const r = await trySelect(row);
|
||||||
|
if (r) return r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 4: Empty search → pick first row; otherwise not found
|
||||||
|
if (!searchLower) {
|
||||||
|
const row = await scanGridRows(selFormNum, '');
|
||||||
|
if (row?.x) {
|
||||||
|
const r = await trySelect(row);
|
||||||
|
if (r) return r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.keyboard.press('Escape');
|
||||||
|
await waitForStable();
|
||||||
|
const searchDesc = typeof search === 'string' ? '"' + search + '"' : JSON.stringify(search);
|
||||||
|
if (hadUnselectableMatch) {
|
||||||
|
return { field: fieldName, error: 'not_selectable',
|
||||||
|
message: 'Found ' + searchDesc + ' in selection form but it is not selectable (group/folder row)' };
|
||||||
|
}
|
||||||
|
return { field: fieldName, error: 'not_found',
|
||||||
|
message: 'No matches in selection form for ' + searchDesc };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detect whether a form is a type selection dialog ("Выбор типа данных").
|
* Detect whether a form is a type selection dialog ("Выбор типа данных").
|
||||||
* Type dialogs appear when selecting a value for a composite-type field.
|
* Type dialogs appear when selecting a value for a composite-type field.
|
||||||
@@ -1286,7 +1441,7 @@ export async function fillFields(fields) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Click a button/hyperlink/tab on the current form. Use {dblclick: true} to double-click (open items from lists). */
|
/** Click a button/hyperlink/tab on the current form. Use {dblclick: true} to double-click (open items from lists). */
|
||||||
export async function clickElement(text, { dblclick } = {}) {
|
export async function clickElement(text, { dblclick, table } = {}) {
|
||||||
ensureConnected();
|
ensureConnected();
|
||||||
await dismissPendingErrors();
|
await dismissPendingErrors();
|
||||||
if (highlightMode) try { await highlight(text); await page.waitForTimeout(500); await unhighlight(); } catch {}
|
if (highlightMode) try { await highlight(text); await page.waitForTimeout(500); await unhighlight(); } catch {}
|
||||||
@@ -1366,8 +1521,16 @@ export async function clickElement(text, { dblclick } = {}) {
|
|||||||
let formNum = await page.evaluate(detectFormScript());
|
let formNum = await page.evaluate(detectFormScript());
|
||||||
if (formNum === null) throw new Error(`clickElement: no form found`);
|
if (formNum === null) throw new Error(`clickElement: no form found`);
|
||||||
|
|
||||||
|
// Pre-resolve grid when table is specified
|
||||||
|
let gridSelector;
|
||||||
|
if (table) {
|
||||||
|
const resolved = await page.evaluate(resolveGridScript(formNum, table));
|
||||||
|
if (resolved.error) throw new Error(`clickElement: table "${table}" not found. Available: ${resolved.available?.map(a => a.name).join(', ') || 'none'}`);
|
||||||
|
gridSelector = resolved.gridSelector;
|
||||||
|
}
|
||||||
|
|
||||||
// Find the target element ID
|
// Find the target element ID
|
||||||
let target = await page.evaluate(findClickTargetScript(formNum, text));
|
let target = await page.evaluate(findClickTargetScript(formNum, text, { tableName: table, gridSelector }));
|
||||||
|
|
||||||
// Retry: if not found, a modal form may still be loading (e.g. after F4).
|
// Retry: if not found, a modal form may still be loading (e.g. after F4).
|
||||||
// Wait up to 2s for a new form to appear and re-detect.
|
// Wait up to 2s for a new form to appear and re-detect.
|
||||||
@@ -1377,7 +1540,7 @@ export async function clickElement(text, { dblclick } = {}) {
|
|||||||
const newForm = await page.evaluate(detectFormScript());
|
const newForm = await page.evaluate(detectFormScript());
|
||||||
if (newForm !== null && newForm !== formNum) {
|
if (newForm !== null && newForm !== formNum) {
|
||||||
formNum = newForm;
|
formNum = newForm;
|
||||||
target = await page.evaluate(findClickTargetScript(formNum, text));
|
target = await page.evaluate(findClickTargetScript(formNum, text, { tableName: table, gridSelector }));
|
||||||
if (!target?.error) break;
|
if (!target?.error) break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1913,12 +2076,20 @@ export async function selectValue(fieldName, searchText, { type } = {}) {
|
|||||||
* @param {boolean} [options.add] - Click "Добавить" to create a new row first
|
* @param {boolean} [options.add] - Click "Добавить" to create a new row first
|
||||||
* @returns {{ filled[], notFilled[]?, form }}
|
* @returns {{ filled[], notFilled[]?, form }}
|
||||||
*/
|
*/
|
||||||
export async function fillTableRow(fields, { tab, add, row } = {}) {
|
export async function fillTableRow(fields, { tab, add, row, table } = {}) {
|
||||||
ensureConnected();
|
ensureConnected();
|
||||||
await dismissPendingErrors();
|
await dismissPendingErrors();
|
||||||
const formNum = await page.evaluate(detectFormScript());
|
const formNum = await page.evaluate(detectFormScript());
|
||||||
if (formNum === null) throw new Error('fillTableRow: no form found');
|
if (formNum === null) throw new Error('fillTableRow: no form found');
|
||||||
|
|
||||||
|
// Pre-resolve grid when table is specified
|
||||||
|
let gridSelector;
|
||||||
|
if (table) {
|
||||||
|
const resolved = await page.evaluate(resolveGridScript(formNum, table));
|
||||||
|
if (resolved.error) throw new Error(`fillTableRow: table "${table}" not found. Available: ${resolved.available?.map(a => a.name).join(', ') || 'none'}`);
|
||||||
|
gridSelector = resolved.gridSelector;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. Switch tab if requested
|
// 1. Switch tab if requested
|
||||||
if (tab) {
|
if (tab) {
|
||||||
@@ -1927,7 +2098,7 @@ export async function fillTableRow(fields, { tab, add, row } = {}) {
|
|||||||
|
|
||||||
// 2. Add new row if requested
|
// 2. Add new row if requested
|
||||||
if (add) {
|
if (add) {
|
||||||
await clickElement('Добавить');
|
await clickElement('Добавить', { table });
|
||||||
// Poll for edit mode (INPUT inside grid) instead of fixed 1000ms wait
|
// Poll for edit mode (INPUT inside grid) instead of fixed 1000ms wait
|
||||||
for (let aw = 0; aw < 6; aw++) {
|
for (let aw = 0; aw < 6; aw++) {
|
||||||
await page.waitForTimeout(150);
|
await page.waitForTimeout(150);
|
||||||
@@ -1945,8 +2116,9 @@ export async function fillTableRow(fields, { tab, add, row } = {}) {
|
|||||||
if (row != null) {
|
if (row != null) {
|
||||||
const fieldKeys = JSON.stringify(Object.keys(fields).map(k => k.toLowerCase()));
|
const fieldKeys = JSON.stringify(Object.keys(fields).map(k => k.toLowerCase()));
|
||||||
const cellCoords = await page.evaluate(`(() => {
|
const cellCoords = await page.evaluate(`(() => {
|
||||||
const grids = [...document.querySelectorAll('.grid')].filter(el => el.offsetWidth > 0);
|
const grid = ${gridSelector
|
||||||
const grid = grids[grids.length - 1];
|
? `document.querySelector(${JSON.stringify(gridSelector)})`
|
||||||
|
: `(() => { const grids = [...document.querySelectorAll('.grid')].filter(el => el.offsetWidth > 0); return grids[grids.length - 1]; })()`};
|
||||||
if (!grid) return { error: 'no_grid' };
|
if (!grid) return { error: 'no_grid' };
|
||||||
const head = grid.querySelector('.gridHead');
|
const head = grid.querySelector('.gridHead');
|
||||||
const body = grid.querySelector('.gridBody');
|
const body = grid.querySelector('.gridBody');
|
||||||
@@ -1980,39 +2152,303 @@ export async function fillTableRow(fields, { tab, add, row } = {}) {
|
|||||||
if (!box) return { error: 'no_cell' };
|
if (!box) return { error: 'no_cell' };
|
||||||
const cell = box.querySelector('.gridBoxText') || box;
|
const cell = box.querySelector('.gridBoxText') || box;
|
||||||
const r = cell.getBoundingClientRect();
|
const r = cell.getBoundingClientRect();
|
||||||
return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) };
|
const currentText = (cell.innerText?.trim() || '').replace(/\u00a0/g, ' ');
|
||||||
|
return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2), currentText };
|
||||||
})()`);
|
})()`);
|
||||||
|
|
||||||
if (cellCoords.error) throw new Error(`fillTableRow: ${cellCoords.error}${cellCoords.total ? ' (total rows: ' + cellCoords.total + ')' : ''}`);
|
if (cellCoords.error) throw new Error(`fillTableRow: ${cellCoords.error}${cellCoords.total ? ' (total rows: ' + cellCoords.total + ')' : ''}`);
|
||||||
|
|
||||||
await page.mouse.dblclick(cellCoords.x, cellCoords.y);
|
// Skip if cell already contains the desired value (single-field optimization)
|
||||||
// Poll for edit mode instead of fixed 500ms wait
|
const firstKey0 = Object.keys(fields)[0];
|
||||||
|
const firstVal0 = typeof fields[firstKey0] === 'object' ? fields[firstKey0].value : String(fields[firstKey0]);
|
||||||
|
let firstFieldSkipped = false;
|
||||||
|
if (cellCoords.currentText && firstVal0 &&
|
||||||
|
cellCoords.currentText.toLowerCase().includes(firstVal0.toLowerCase())) {
|
||||||
|
firstFieldSkipped = true;
|
||||||
|
if (Object.keys(fields).length === 1) {
|
||||||
|
return [{ field: firstKey0, ok: true, method: 'skip', value: cellCoords.currentText }];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Click first (tree grids enter edit on single click; dblclick toggles expand/collapse).
|
||||||
|
// Then escalate: dblclick → F4 if needed.
|
||||||
|
await page.mouse.click(cellCoords.x, cellCoords.y);
|
||||||
let inEdit = false;
|
let inEdit = false;
|
||||||
for (let dw = 0; dw < 5; dw++) {
|
let directEditForm = null;
|
||||||
|
for (let dw = 0; dw < 4; dw++) {
|
||||||
await page.waitForTimeout(150);
|
await page.waitForTimeout(150);
|
||||||
inEdit = await page.evaluate(`(() => {
|
inEdit = await page.evaluate(`(() => {
|
||||||
const f = document.activeElement;
|
const f = document.activeElement;
|
||||||
return f && f.tagName === 'INPUT';
|
return f && f.tagName === 'INPUT';
|
||||||
})()`);
|
})()`);
|
||||||
if (inEdit) break;
|
if (inEdit) break;
|
||||||
|
directEditForm = await page.evaluate(`(() => {
|
||||||
|
const forms = {};
|
||||||
|
document.querySelectorAll('[id]').forEach(el => {
|
||||||
|
if (el.offsetWidth === 0 && el.offsetHeight === 0) return;
|
||||||
|
const m = el.id.match(/^form(\\d+)_/);
|
||||||
|
if (m) forms[m[1]] = true;
|
||||||
|
});
|
||||||
|
const nums = Object.keys(forms).map(Number).filter(n => n > ${formNum});
|
||||||
|
return nums.length > 0 ? Math.max(...nums) : null;
|
||||||
|
})()`);
|
||||||
|
if (directEditForm !== null) break;
|
||||||
}
|
}
|
||||||
if (!inEdit) throw new Error(`fillTableRow: double-click on row ${row} did not enter edit mode`);
|
// Click didn't enter edit — try dblclick (works for flat grids)
|
||||||
}
|
if (!inEdit && directEditForm === null) {
|
||||||
|
await page.mouse.dblclick(cellCoords.x, cellCoords.y);
|
||||||
// 3. Verify we're in grid edit mode (active INPUT inside a .grid)
|
for (let dw = 0; dw < 4; dw++) {
|
||||||
const editCheck = await page.evaluate(`(() => {
|
await page.waitForTimeout(150);
|
||||||
const f = document.activeElement;
|
inEdit = await page.evaluate(`(() => {
|
||||||
if (!f || f.tagName !== 'INPUT') return { inEdit: false, tag: f?.tagName };
|
const f = document.activeElement;
|
||||||
let node = f;
|
return f && f.tagName === 'INPUT';
|
||||||
while (node) {
|
})()`);
|
||||||
if (node.classList?.contains('grid')) return { inEdit: true };
|
if (inEdit) break;
|
||||||
node = node.parentElement;
|
directEditForm = await page.evaluate(`(() => {
|
||||||
|
const forms = {};
|
||||||
|
document.querySelectorAll('[id]').forEach(el => {
|
||||||
|
if (el.offsetWidth === 0 && el.offsetHeight === 0) return;
|
||||||
|
const m = el.id.match(/^form(\\d+)_/);
|
||||||
|
if (m) forms[m[1]] = true;
|
||||||
|
});
|
||||||
|
const nums = Object.keys(forms).map(Number).filter(n => n > ${formNum});
|
||||||
|
return nums.length > 0 ? Math.max(...nums) : null;
|
||||||
|
})()`);
|
||||||
|
if (directEditForm !== null) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Still nothing — try F4 (opens selection for direct-edit cells)
|
||||||
|
if (!inEdit && directEditForm === null) {
|
||||||
|
await page.keyboard.press('F4');
|
||||||
|
for (let fw = 0; fw < 8; fw++) {
|
||||||
|
await page.waitForTimeout(200);
|
||||||
|
inEdit = await page.evaluate(`(() => {
|
||||||
|
const f = document.activeElement;
|
||||||
|
return f && f.tagName === 'INPUT';
|
||||||
|
})()`);
|
||||||
|
if (inEdit) break;
|
||||||
|
directEditForm = await page.evaluate(`(() => {
|
||||||
|
const forms = {};
|
||||||
|
document.querySelectorAll('[id]').forEach(el => {
|
||||||
|
if (el.offsetWidth === 0 && el.offsetHeight === 0) return;
|
||||||
|
const m = el.id.match(/^form(\\d+)_/);
|
||||||
|
if (m) forms[m[1]] = true;
|
||||||
|
});
|
||||||
|
const nums = Object.keys(forms).map(Number).filter(n => n > ${formNum});
|
||||||
|
return nums.length > 0 ? Math.max(...nums) : null;
|
||||||
|
})()`);
|
||||||
|
if (directEditForm !== null) break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return { inEdit: false, hint: 'input not inside grid' };
|
|
||||||
})()`);
|
|
||||||
|
|
||||||
if (!editCheck.inEdit) {
|
// When click entered INPUT mode but no selection form yet — try F4 (tree grid ref fields)
|
||||||
throw new Error('fillTableRow: not in grid edit mode. Use add:true or click a cell first.');
|
if (inEdit && directEditForm === null) {
|
||||||
|
await page.keyboard.press('F4');
|
||||||
|
for (let fw = 0; fw < 8; fw++) {
|
||||||
|
await page.waitForTimeout(200);
|
||||||
|
directEditForm = await page.evaluate(`(() => {
|
||||||
|
const forms = {};
|
||||||
|
document.querySelectorAll('[id]').forEach(el => {
|
||||||
|
if (el.offsetWidth === 0 && el.offsetHeight === 0) return;
|
||||||
|
const m = el.id.match(/^form(\\d+)_/);
|
||||||
|
if (m) forms[m[1]] = true;
|
||||||
|
});
|
||||||
|
const nums = Object.keys(forms).map(Number).filter(n => n > ${formNum});
|
||||||
|
return nums.length > 0 ? Math.max(...nums) : null;
|
||||||
|
})()`);
|
||||||
|
if (directEditForm !== null) break;
|
||||||
|
}
|
||||||
|
// If F4 didn't open a selection form, the cell is a plain text field — fall through to Tab loop
|
||||||
|
}
|
||||||
|
|
||||||
|
// Direct-edit mode: selection form opened on dblclick/F4 (e.g. tree grid with immediate editing).
|
||||||
|
// Handle each field by picking from selection form, then dblclick next cell.
|
||||||
|
if (directEditForm !== null) {
|
||||||
|
const pending = new Map();
|
||||||
|
for (const [key, val] of Object.entries(fields)) {
|
||||||
|
if (val && typeof val === 'object' && 'value' in val) {
|
||||||
|
pending.set(key, { value: String(val.value), type: val.type || null, filled: false });
|
||||||
|
} else {
|
||||||
|
pending.set(key, { value: String(val), type: null, filled: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
// Helper: handle type dialog + pick from selection form
|
||||||
|
async function directEditPick(openedForm, key, info) {
|
||||||
|
let selForm = openedForm;
|
||||||
|
// Check if opened form is a type selection dialog (composite type field)
|
||||||
|
if (await isTypeDialog(selForm)) {
|
||||||
|
if (info.type) {
|
||||||
|
await pickFromTypeDialog(selForm, info.type);
|
||||||
|
await waitForStable(selForm);
|
||||||
|
// After type selection, detect the actual selection form
|
||||||
|
selForm = await page.evaluate(`(() => {
|
||||||
|
const forms = {};
|
||||||
|
document.querySelectorAll('[id]').forEach(el => {
|
||||||
|
if (el.offsetWidth === 0 && el.offsetHeight === 0) return;
|
||||||
|
const m = el.id.match(/^form(\\d+)_/);
|
||||||
|
if (m) forms[m[1]] = true;
|
||||||
|
});
|
||||||
|
const nums = Object.keys(forms).map(Number).filter(n => n > ${formNum});
|
||||||
|
return nums.length > 0 ? Math.max(...nums) : null;
|
||||||
|
})()`);
|
||||||
|
if (selForm === null) {
|
||||||
|
return { field: key, error: 'no_selection_after_type', message: `Type selected but no selection form opened for "${key}"` };
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No type specified — close type dialog and report error
|
||||||
|
await page.keyboard.press('Escape');
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
return { field: key, error: 'composite_type', message: `Composite type field "${key}" requires {value, type}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const pr = await pickFromSelectionForm(selForm, key, info.value, formNum);
|
||||||
|
return pr.ok ? { field: key, ok: true, method: 'form' } : { field: key, error: pr.error, message: pr.message };
|
||||||
|
}
|
||||||
|
|
||||||
|
// First field: selection form is already open from the dblclick above
|
||||||
|
const firstKey = Object.keys(fields)[0];
|
||||||
|
const firstInfo = pending.get(firstKey);
|
||||||
|
if (firstFieldSkipped) {
|
||||||
|
firstInfo.filled = true;
|
||||||
|
results.push({ field: firstKey, ok: true, method: 'skip', value: cellCoords.currentText });
|
||||||
|
// Close the selection form that opened from the click
|
||||||
|
await page.keyboard.press('Escape');
|
||||||
|
await waitForStable(formNum);
|
||||||
|
} else {
|
||||||
|
const pickResult = await directEditPick(directEditForm, firstKey, firstInfo);
|
||||||
|
firstInfo.filled = true;
|
||||||
|
results.push(pickResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remaining fields: dblclick on each column cell individually
|
||||||
|
for (const [key, info] of pending) {
|
||||||
|
if (info.filled) continue;
|
||||||
|
// Find column for this key and dblclick on it
|
||||||
|
const nextCoords = await page.evaluate(`(() => {
|
||||||
|
const grid = ${gridSelector
|
||||||
|
? `document.querySelector(${JSON.stringify(gridSelector)})`
|
||||||
|
: `(() => { const grids = [...document.querySelectorAll('.grid')].filter(el => el.offsetWidth > 0); return grids[grids.length - 1]; })()`};
|
||||||
|
if (!grid) return null;
|
||||||
|
const head = grid.querySelector('.gridHead');
|
||||||
|
const body = grid.querySelector('.gridBody');
|
||||||
|
if (!head || !body) return null;
|
||||||
|
const headLine = head.querySelector('.gridLine') || head;
|
||||||
|
const cols = [];
|
||||||
|
[...headLine.children].forEach((box, i) => {
|
||||||
|
if (box.offsetWidth === 0) return;
|
||||||
|
const t = box.querySelector('.gridBoxText');
|
||||||
|
cols.push({ idx: i, text: ((t || box).innerText?.trim() || '').toLowerCase() });
|
||||||
|
});
|
||||||
|
const kl = ${JSON.stringify(key.toLowerCase())};
|
||||||
|
const klNoSpace = kl.replace(/[\\s\\-]+/g, '');
|
||||||
|
let colIdx = -1;
|
||||||
|
const exact = cols.find(c => c.text === kl);
|
||||||
|
if (exact) colIdx = exact.idx;
|
||||||
|
else {
|
||||||
|
const inc = cols.find(c => c.text.includes(kl) || kl.includes(c.text)
|
||||||
|
|| c.text.includes(klNoSpace) || klNoSpace.includes(c.text));
|
||||||
|
if (inc) colIdx = inc.idx;
|
||||||
|
}
|
||||||
|
if (colIdx < 0) return null;
|
||||||
|
const rows = [...body.querySelectorAll('.gridLine')];
|
||||||
|
if (${row} >= rows.length) return null;
|
||||||
|
const line = rows[${row}];
|
||||||
|
const boxes = [...line.children].filter(b => b.offsetWidth > 0 && !b.classList.contains('gridBoxComp'));
|
||||||
|
const box = boxes[colIdx];
|
||||||
|
if (!box) return null;
|
||||||
|
const cell = box.querySelector('.gridBoxText') || box;
|
||||||
|
const r = cell.getBoundingClientRect();
|
||||||
|
const currentText = (cell.innerText?.trim() || '').replace(/\\u00a0/g, ' ');
|
||||||
|
return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2), currentText };
|
||||||
|
})()`);
|
||||||
|
if (!nextCoords) {
|
||||||
|
info.filled = true;
|
||||||
|
results.push({ field: key, error: 'column_not_found', message: `Column for "${key}" not found` });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Skip if cell already contains the desired value
|
||||||
|
if (nextCoords.currentText && info.value &&
|
||||||
|
nextCoords.currentText.toLowerCase().includes(info.value.toLowerCase())) {
|
||||||
|
info.filled = true;
|
||||||
|
results.push({ field: key, ok: true, method: 'skip', value: nextCoords.currentText });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
await page.mouse.dblclick(nextCoords.x, nextCoords.y);
|
||||||
|
// Poll for selection form (with F4 fallback if dblclick didn't open it)
|
||||||
|
let selForm = null;
|
||||||
|
for (let attempt = 0; attempt < 2 && selForm === null; attempt++) {
|
||||||
|
if (attempt === 1) await page.keyboard.press('F4'); // F4 fallback
|
||||||
|
for (let sw = 0; sw < 6; sw++) {
|
||||||
|
await page.waitForTimeout(200);
|
||||||
|
selForm = await page.evaluate(`(() => {
|
||||||
|
const forms = {};
|
||||||
|
document.querySelectorAll('[id]').forEach(el => {
|
||||||
|
if (el.offsetWidth === 0 && el.offsetHeight === 0) return;
|
||||||
|
const m = el.id.match(/^form(\\d+)_/);
|
||||||
|
if (m) forms[m[1]] = true;
|
||||||
|
});
|
||||||
|
const nums = Object.keys(forms).map(Number).filter(n => n > ${formNum});
|
||||||
|
return nums.length > 0 ? Math.max(...nums) : null;
|
||||||
|
})()`);
|
||||||
|
if (selForm !== null) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (selForm === null) {
|
||||||
|
info.filled = true;
|
||||||
|
results.push({ field: key, error: 'no_selection_form', message: `Dblclick on "${key}" did not open selection form` });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const pr = await directEditPick(selForm, key, info);
|
||||||
|
info.filled = true;
|
||||||
|
results.push(pr);
|
||||||
|
}
|
||||||
|
// Commit the edit: click on a different row (Escape cancels in tree grids).
|
||||||
|
// Find the first visible row that is NOT the edited row and click it.
|
||||||
|
const commitCoords = await page.evaluate(`(() => {
|
||||||
|
const grid = ${gridSelector
|
||||||
|
? `document.querySelector(${JSON.stringify(gridSelector)})`
|
||||||
|
: `(() => { const grids = [...document.querySelectorAll('.grid')].filter(el => el.offsetWidth > 0); return grids[grids.length - 1]; })()`};
|
||||||
|
if (!grid) return null;
|
||||||
|
const body = grid.querySelector('.gridBody');
|
||||||
|
if (!body) return null;
|
||||||
|
const rows = [...body.querySelectorAll('.gridLine')];
|
||||||
|
const otherIdx = ${row} === 0 ? 1 : 0;
|
||||||
|
const other = rows[otherIdx];
|
||||||
|
if (!other) return null;
|
||||||
|
const visBoxes = [...other.children].filter(b => b.offsetWidth > 0 && !b.classList.contains('gridBoxComp'));
|
||||||
|
const box = visBoxes.length > 1 ? visBoxes[1] : visBoxes[0];
|
||||||
|
if (!box) return null;
|
||||||
|
const r = box.getBoundingClientRect();
|
||||||
|
return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) };
|
||||||
|
})()`);
|
||||||
|
if (commitCoords) {
|
||||||
|
await page.mouse.click(commitCoords.x, commitCoords.y);
|
||||||
|
} else {
|
||||||
|
await page.keyboard.press('Escape');
|
||||||
|
}
|
||||||
|
await waitForStable(formNum);
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!inEdit) throw new Error(`fillTableRow: click on row ${row} did not enter edit mode`);
|
||||||
|
} else {
|
||||||
|
// No row specified — verify we're in grid edit mode (active INPUT inside a .grid or .gridContent)
|
||||||
|
const editCheck = await page.evaluate(`(() => {
|
||||||
|
const f = document.activeElement;
|
||||||
|
if (!f || f.tagName !== 'INPUT') return { inEdit: false, tag: f?.tagName };
|
||||||
|
let node = f;
|
||||||
|
while (node) {
|
||||||
|
if (node.classList?.contains('grid') || node.classList?.contains('gridContent')) return { inEdit: true };
|
||||||
|
node = node.parentElement;
|
||||||
|
}
|
||||||
|
return { inEdit: false, hint: 'input not inside grid' };
|
||||||
|
})()`);
|
||||||
|
|
||||||
|
if (!editCheck.inEdit) {
|
||||||
|
throw new Error('fillTableRow: not in grid edit mode. Use add:true or click a cell first.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Prepare pending fields for fuzzy matching
|
// 4. Prepare pending fields for fuzzy matching
|
||||||
@@ -2079,8 +2515,8 @@ export async function fillTableRow(fields, { tab, add, row } = {}) {
|
|||||||
matchedKey = key;
|
matchedKey = key;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// CamelCase cell names have no spaces — try matching without spaces
|
// CamelCase cell names have no spaces/dashes — try matching without spaces and dashes
|
||||||
const klNoSpace = kl.replace(/\s+/g, '');
|
const klNoSpace = kl.replace(/[\s\-]+/g, '');
|
||||||
if (klNoSpace && (cellLower.endsWith(klNoSpace) || cellLower.includes(klNoSpace))) {
|
if (klNoSpace && (cellLower.endsWith(klNoSpace) || cellLower.includes(klNoSpace))) {
|
||||||
matchedKey = key;
|
matchedKey = key;
|
||||||
break;
|
break;
|
||||||
@@ -2522,6 +2958,50 @@ export async function fillTableRow(fields, { tab, add, row } = {}) {
|
|||||||
// Tab already pressed — we're on next cell
|
// Tab already pressed — we're on next cell
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Commit the new row: click on a different row or outside the grid.
|
||||||
|
// Without this, the row stays in "uncommitted add" state and a subsequent
|
||||||
|
// Escape (e.g. from closeForm) would cancel the entire row.
|
||||||
|
const commitTarget = await page.evaluate(`(() => {
|
||||||
|
// Find the active grid
|
||||||
|
const grid = ${gridSelector
|
||||||
|
? `document.querySelector(${JSON.stringify(gridSelector)})`
|
||||||
|
: `(() => { const grids = [...document.querySelectorAll('.grid')].filter(el => el.offsetWidth > 0); return grids[grids.length - 1]; })()`};
|
||||||
|
if (!grid) return null;
|
||||||
|
const body = grid.querySelector('.gridBody');
|
||||||
|
if (!body) return null;
|
||||||
|
const rows = [...body.querySelectorAll('.gridLine')];
|
||||||
|
// Find the currently active row (contains the focused input)
|
||||||
|
const activeInput = document.activeElement;
|
||||||
|
let activeRowIdx = -1;
|
||||||
|
if (activeInput) {
|
||||||
|
for (let i = 0; i < rows.length; i++) {
|
||||||
|
if (rows[i].contains(activeInput)) { activeRowIdx = i; break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Click a DIFFERENT row to commit
|
||||||
|
const targetIdx = activeRowIdx === 0 ? 1 : 0;
|
||||||
|
const target = rows[targetIdx];
|
||||||
|
if (target) {
|
||||||
|
const visBoxes = [...target.children].filter(b => b.offsetWidth > 0 && !b.classList.contains('gridBoxComp'));
|
||||||
|
const box = visBoxes.length > 1 ? visBoxes[1] : visBoxes[0];
|
||||||
|
if (box) {
|
||||||
|
const r = box.getBoundingClientRect();
|
||||||
|
return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fallback: click the grid header
|
||||||
|
const head = grid.querySelector('.gridHead');
|
||||||
|
if (head) {
|
||||||
|
const r = head.getBoundingClientRect();
|
||||||
|
return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})()`);
|
||||||
|
if (commitTarget) {
|
||||||
|
await page.mouse.click(commitTarget.x, commitTarget.y);
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
}
|
||||||
|
|
||||||
// Dismiss any leftover error modals
|
// Dismiss any leftover error modals
|
||||||
const err = await checkForErrors();
|
const err = await checkForErrors();
|
||||||
if (err?.modal) {
|
if (err?.modal) {
|
||||||
@@ -2553,12 +3033,20 @@ export async function fillTableRow(fields, { tab, add, row } = {}) {
|
|||||||
* @param {string} [options.tab] - Switch to this form tab before operating
|
* @param {string} [options.tab] - Switch to this form tab before operating
|
||||||
* @returns {{ deleted, rowsBefore, rowsAfter, form }}
|
* @returns {{ deleted, rowsBefore, rowsAfter, form }}
|
||||||
*/
|
*/
|
||||||
export async function deleteTableRow(row, { tab } = {}) {
|
export async function deleteTableRow(row, { tab, table } = {}) {
|
||||||
ensureConnected();
|
ensureConnected();
|
||||||
await dismissPendingErrors();
|
await dismissPendingErrors();
|
||||||
const formNum = await page.evaluate(detectFormScript());
|
const formNum = await page.evaluate(detectFormScript());
|
||||||
if (formNum === null) throw new Error('deleteTableRow: no form found');
|
if (formNum === null) throw new Error('deleteTableRow: no form found');
|
||||||
|
|
||||||
|
// Pre-resolve grid when table is specified
|
||||||
|
let gridSelector;
|
||||||
|
if (table) {
|
||||||
|
const resolved = await page.evaluate(resolveGridScript(formNum, table));
|
||||||
|
if (resolved.error) throw new Error(`deleteTableRow: table "${table}" not found. Available: ${resolved.available?.map(a => a.name).join(', ') || 'none'}`);
|
||||||
|
gridSelector = resolved.gridSelector;
|
||||||
|
}
|
||||||
|
|
||||||
// 1. Switch tab if requested
|
// 1. Switch tab if requested
|
||||||
if (tab) {
|
if (tab) {
|
||||||
await clickElement(tab);
|
await clickElement(tab);
|
||||||
@@ -2567,17 +3055,21 @@ export async function deleteTableRow(row, { tab } = {}) {
|
|||||||
|
|
||||||
// 2. Find the target row and click to select it
|
// 2. Find the target row and click to select it
|
||||||
const cellCoords = await page.evaluate(`(() => {
|
const cellCoords = await page.evaluate(`(() => {
|
||||||
const grids = [...document.querySelectorAll('.grid')].filter(el => el.offsetWidth > 0);
|
const grid = ${gridSelector
|
||||||
const grid = grids[grids.length - 1];
|
? `document.querySelector(${JSON.stringify(gridSelector)})`
|
||||||
|
: `(() => { const grids = [...document.querySelectorAll('.grid')].filter(el => el.offsetWidth > 0); return grids[grids.length - 1]; })()`};
|
||||||
if (!grid) return { error: 'no_grid' };
|
if (!grid) return { error: 'no_grid' };
|
||||||
const body = grid.querySelector('.gridBody');
|
const body = grid.querySelector('.gridBody');
|
||||||
if (!body) return { error: 'no_grid_body' };
|
if (!body) return { error: 'no_grid_body' };
|
||||||
const rows = [...body.querySelectorAll('.gridLine')];
|
const rows = [...body.querySelectorAll('.gridLine')];
|
||||||
if (${row} >= rows.length) return { error: 'row_out_of_range', total: rows.length };
|
if (${row} >= rows.length) return { error: 'row_out_of_range', total: rows.length };
|
||||||
const line = rows[${row}];
|
const line = rows[${row}];
|
||||||
const cells = [...line.querySelectorAll('.gridBoxText')];
|
// Use visible gridBox containers (not gridBoxText) to avoid clicking checkboxes
|
||||||
const cell = cells.length > 1 ? cells[1] : cells[0];
|
const boxes = [...line.children].filter(b => b.offsetWidth > 0 && !b.classList.contains('gridBoxComp'));
|
||||||
if (!cell) return { error: 'no_cell' };
|
// Skip first column (row number / checkbox) — pick second visible box
|
||||||
|
const box = boxes.length > 1 ? boxes[1] : boxes[0];
|
||||||
|
if (!box) return { error: 'no_cell' };
|
||||||
|
const cell = box.querySelector('.gridBoxText') || box;
|
||||||
const r = cell.getBoundingClientRect();
|
const r = cell.getBoundingClientRect();
|
||||||
return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2), total: rows.length };
|
return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2), total: rows.length };
|
||||||
})()`);
|
})()`);
|
||||||
@@ -2596,8 +3088,9 @@ export async function deleteTableRow(row, { tab } = {}) {
|
|||||||
|
|
||||||
// 4. Count rows after deletion
|
// 4. Count rows after deletion
|
||||||
const rowsAfter = await page.evaluate(`(() => {
|
const rowsAfter = await page.evaluate(`(() => {
|
||||||
const grids = [...document.querySelectorAll('.grid')].filter(el => el.offsetWidth > 0);
|
const grid = ${gridSelector
|
||||||
const grid = grids[grids.length - 1];
|
? `document.querySelector(${JSON.stringify(gridSelector)})`
|
||||||
|
: `(() => { const grids = [...document.querySelectorAll('.grid')].filter(el => el.offsetWidth > 0); return grids[grids.length - 1]; })()`};
|
||||||
if (!grid) return 0;
|
if (!grid) return 0;
|
||||||
const body = grid.querySelector('.gridBody');
|
const body = grid.querySelector('.gridBody');
|
||||||
return body ? body.querySelectorAll('.gridLine').length : 0;
|
return body ? body.querySelectorAll('.gridLine').length : 0;
|
||||||
@@ -2635,20 +3128,40 @@ export async function filterList(text, { field, exact } = {}) {
|
|||||||
.find(el => el.offsetWidth > 0 && /Строк[аи]Поиска|SearchString/i.test(el.id));
|
.find(el => el.offsetWidth > 0 && /Строк[аи]Поиска|SearchString/i.test(el.id));
|
||||||
return el ? el.id : null;
|
return el ? el.id : null;
|
||||||
})()`);
|
})()`);
|
||||||
if (!searchId) throw new Error('filterList: no search input found on this form');
|
|
||||||
|
|
||||||
await page.click(`[id="${searchId}"]`);
|
if (searchId) {
|
||||||
await page.waitForTimeout(200);
|
await page.click(`[id="${searchId}"]`);
|
||||||
await page.keyboard.press('Control+A');
|
await page.waitForTimeout(200);
|
||||||
await page.evaluate(`navigator.clipboard.writeText(${JSON.stringify(String(text))})`);
|
await page.keyboard.press('Control+A');
|
||||||
await page.keyboard.press('Control+V');
|
await page.evaluate(`navigator.clipboard.writeText(${JSON.stringify(String(text))})`);
|
||||||
|
await page.keyboard.press('Control+V');
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
await page.keyboard.press('Enter');
|
||||||
|
await waitForStable(formNum);
|
||||||
|
|
||||||
|
const state = await getFormState();
|
||||||
|
state.filtered = { type: 'search', text };
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No search input — Ctrl+F opens advanced search on such forms.
|
||||||
|
// Click first grid cell then fall through to advanced search path below.
|
||||||
|
const firstCell = await page.evaluate(`(() => {
|
||||||
|
const p = 'form${formNum}_';
|
||||||
|
const grid = [...document.querySelectorAll('[id^="' + p + '"].grid, [id^="' + p + '"] .grid')]
|
||||||
|
.find(g => g.offsetWidth > 0);
|
||||||
|
if (!grid) return null;
|
||||||
|
const rows = [...grid.querySelectorAll('.gridBody .gridLine')];
|
||||||
|
if (!rows.length) return null;
|
||||||
|
const cells = [...rows[0].querySelectorAll('.gridBox')];
|
||||||
|
if (!cells.length) return null;
|
||||||
|
const r = cells[0].getBoundingClientRect();
|
||||||
|
return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) };
|
||||||
|
})()`);
|
||||||
|
if (!firstCell) throw new Error('filterList: no search input and no grid found on this form');
|
||||||
|
await page.mouse.click(firstCell.x, firstCell.y);
|
||||||
await page.waitForTimeout(300);
|
await page.waitForTimeout(300);
|
||||||
await page.keyboard.press('Enter');
|
field = ''; // fall through to advanced search, skip DLB (empty field = keep auto-selected)
|
||||||
await waitForStable(formNum);
|
|
||||||
|
|
||||||
const state = await getFormState();
|
|
||||||
state.filtered = { type: 'search', text };
|
|
||||||
return state;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Advanced search: click target column cell → Alt+F → fill Pattern → Найти ---
|
// --- Advanced search: click target column cell → Alt+F → fill Pattern → Найти ---
|
||||||
@@ -2724,7 +3237,8 @@ export async function filterList(text, { field, exact } = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2b. If column wasn't in the grid, change FieldSelector via DLB dropdown
|
// 2b. If column wasn't in the grid, change FieldSelector via DLB dropdown
|
||||||
if (needDlb) {
|
// Skip DLB when field is empty (fallback from no-search-input path — keep auto-selected field)
|
||||||
|
if (needDlb && field) {
|
||||||
const fsInfo = await page.evaluate(`(() => {
|
const fsInfo = await page.evaluate(`(() => {
|
||||||
const p = 'form' + ${JSON.stringify(String(dialogForm))} + '_';
|
const p = 'form' + ${JSON.stringify(String(dialogForm))} + '_';
|
||||||
const fsInput = [...document.querySelectorAll('input.editInput[id^="' + p + '"]')]
|
const fsInput = [...document.querySelectorAll('input.editInput[id^="' + p + '"]')]
|
||||||
|
|||||||
@@ -185,24 +185,35 @@ const READ_FORM_FN = `function readForm(p) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Table/grid — pick the first VISIBLE grid (tab switching hides inactive grids)
|
// Tables/grids — collect ALL visible grids
|
||||||
const grid = [...document.querySelectorAll('[id^="' + p + '"].grid, [id^="' + p + '"] .grid')]
|
const allGrids = [...document.querySelectorAll('[id^="' + p + '"].grid, [id^="' + p + '"] .grid')]
|
||||||
.find(g => g.offsetWidth > 0 && g.offsetHeight > 0);
|
.filter(g => g.offsetWidth > 0 && g.offsetHeight > 0);
|
||||||
if (grid) {
|
if (allGrids.length > 0) {
|
||||||
const head = grid.querySelector('.gridHead');
|
const tables = allGrids.map(grid => {
|
||||||
const body = grid.querySelector('.gridBody');
|
const name = grid.id ? grid.id.replace(p, '') : '';
|
||||||
const columns = [];
|
const head = grid.querySelector('.gridHead');
|
||||||
if (head) {
|
const body = grid.querySelector('.gridBody');
|
||||||
const headLine = head.querySelector('.gridLine') || head;
|
const columns = [];
|
||||||
[...headLine.children].forEach(box => {
|
if (head) {
|
||||||
if (box.offsetWidth === 0) return;
|
const headLine = head.querySelector('.gridLine') || head;
|
||||||
const textEl = box.querySelector('.gridBoxText');
|
[...headLine.children].forEach(box => {
|
||||||
const text = (textEl || box).innerText?.trim().replace(/\\n/g, ' ') || '';
|
if (box.offsetWidth === 0) return;
|
||||||
if (text) columns.push(text);
|
const textEl = box.querySelector('.gridBoxText');
|
||||||
});
|
const text = (textEl || box).innerText?.trim().replace(/\\n/g, ' ') || '';
|
||||||
}
|
if (text) columns.push(text);
|
||||||
const rowCount = body ? body.querySelectorAll('.gridLine').length : 0;
|
});
|
||||||
result.table = { present: true, columns, rowCount };
|
}
|
||||||
|
const rowCount = body ? body.querySelectorAll('.gridLine').length : 0;
|
||||||
|
// Visual label from group title (e.g. "Входящие:" for grid "Входящие")
|
||||||
|
const titleEl = document.getElementById(p + name + '#title_div')
|
||||||
|
|| document.getElementById(p + 'Группа' + name + '#title_div');
|
||||||
|
const label = titleEl ? (titleEl.innerText?.trim().replace(/:\\s*$/, '').replace(/\\u00a0/g, ' ') || null) : null;
|
||||||
|
return { name, columns, rowCount, ...(label ? { label } : {}) };
|
||||||
|
});
|
||||||
|
result.tables = tables;
|
||||||
|
// Backward compat: table = first grid summary
|
||||||
|
const first = tables[0];
|
||||||
|
result.table = { present: true, columns: first.columns, rowCount: first.rowCount };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Active filters (train badges above grid: *СостояниеПросмотра)
|
// Active filters (train badges above grid: *СостояниеПросмотра)
|
||||||
@@ -356,18 +367,81 @@ export function readFormScript(formNum) {
|
|||||||
})()`;
|
})()`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a specific grid by semantic name (table parameter).
|
||||||
|
* Cascade: exact gridName match → gridName contains → column contains.
|
||||||
|
* Returns { gridSelector, gridId, gridName, gridIndex, columns } or { error, available }.
|
||||||
|
*/
|
||||||
|
export function resolveGridScript(formNum, tableName) {
|
||||||
|
const p = `form${formNum}_`;
|
||||||
|
return `(() => {
|
||||||
|
const p = ${JSON.stringify(p)};
|
||||||
|
const target = ${JSON.stringify(tableName.toLowerCase().replace(/ё/g, 'е'))};
|
||||||
|
const norm = s => (s || '').replace(/ё/gi, 'е');
|
||||||
|
const allGrids = [...document.querySelectorAll('[id^="' + p + '"].grid, [id^="' + p + '"] .grid')]
|
||||||
|
.filter(g => g.offsetWidth > 0 && g.offsetHeight > 0);
|
||||||
|
if (!allGrids.length) return { error: 'no_grids', message: 'No grids found on form' };
|
||||||
|
const infos = allGrids.map((g, idx) => {
|
||||||
|
const gridId = g.id || '';
|
||||||
|
const gridName = gridId.replace(p, '');
|
||||||
|
const head = g.querySelector('.gridHead');
|
||||||
|
const columns = [];
|
||||||
|
if (head) {
|
||||||
|
const headLine = head.querySelector('.gridLine') || head;
|
||||||
|
[...headLine.children].forEach(box => {
|
||||||
|
if (box.offsetWidth === 0) return;
|
||||||
|
const textEl = box.querySelector('.gridBoxText');
|
||||||
|
const text = (textEl || box).innerText?.trim().replace(/\\n/g, ' ') || '';
|
||||||
|
if (text) columns.push(text);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Visual label from group title element
|
||||||
|
const titleEl = document.getElementById(p + gridName + '#title_div')
|
||||||
|
|| document.getElementById(p + 'Группа' + gridName + '#title_div');
|
||||||
|
const label = titleEl ? (titleEl.innerText?.trim().replace(/:\s*$/, '').replace(/\u00a0/g, ' ') || '') : '';
|
||||||
|
return { idx, gridId, gridName, label, columns, el: g };
|
||||||
|
});
|
||||||
|
// 1. Exact gridName match (case-insensitive)
|
||||||
|
let found = infos.find(i => norm(i.gridName).toLowerCase() === target);
|
||||||
|
// 2. Exact label match
|
||||||
|
if (!found) found = infos.find(i => i.label && norm(i.label).toLowerCase() === target);
|
||||||
|
// 3. gridName contains target
|
||||||
|
if (!found) found = infos.find(i => norm(i.gridName).toLowerCase().includes(target));
|
||||||
|
// 4. Label contains target
|
||||||
|
if (!found) found = infos.find(i => i.label && norm(i.label).toLowerCase().includes(target));
|
||||||
|
// 5. Any column contains target
|
||||||
|
if (!found) found = infos.find(i => i.columns.some(c => norm(c).toLowerCase().includes(target)));
|
||||||
|
if (found) {
|
||||||
|
return {
|
||||||
|
gridSelector: found.gridId ? '#' + CSS.escape(found.gridId) : null,
|
||||||
|
gridId: found.gridId,
|
||||||
|
gridName: found.gridName,
|
||||||
|
gridIndex: found.idx,
|
||||||
|
columns: found.columns
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
error: 'not_found',
|
||||||
|
message: 'Table "' + ${JSON.stringify(tableName)} + '" not found',
|
||||||
|
available: infos.map(i => ({ name: i.gridName, ...(i.label ? { label: i.label } : {}), columns: i.columns }))
|
||||||
|
};
|
||||||
|
})()`;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read table/grid data with pagination.
|
* Read table/grid data with pagination.
|
||||||
* Parses grid.innerText — \n separates rows, \t separates cells.
|
* Parses grid.innerText — \n separates rows, \t separates cells.
|
||||||
* First row = column headers.
|
* First row = column headers.
|
||||||
* Returns { name, columns[], rows[{col:val}], total, offset, shown }.
|
* Returns { name, columns[], rows[{col:val}], total, offset, shown }.
|
||||||
*/
|
*/
|
||||||
export function readTableScript(formNum, { maxRows = 20, offset = 0 } = {}) {
|
export function readTableScript(formNum, { maxRows = 20, offset = 0, gridSelector } = {}) {
|
||||||
const p = `form${formNum}_`;
|
const p = `form${formNum}_`;
|
||||||
return `(() => {
|
return `(() => {
|
||||||
const p = ${JSON.stringify(p)};
|
const p = ${JSON.stringify(p)};
|
||||||
const grid = [...document.querySelectorAll('[id^="' + p + '"].grid, [id^="' + p + '"] .grid')]
|
const grid = ${gridSelector
|
||||||
.find(g => g.offsetWidth > 0 && g.offsetHeight > 0);
|
? `document.querySelector(${JSON.stringify(gridSelector)})`
|
||||||
|
: `[...document.querySelectorAll('[id^="' + p + '"].grid, [id^="' + p + '"] .grid')]
|
||||||
|
.find(g => g.offsetWidth > 0 && g.offsetHeight > 0)`};
|
||||||
if (!grid) return { error: 'no_table', message: 'No table found on form ${formNum}' };
|
if (!grid) return { error: 'no_table', message: 'No table found on form ${formNum}' };
|
||||||
const name = grid.id ? grid.id.replace(p, '') : '';
|
const name = grid.id ? grid.id.replace(p, '') : '';
|
||||||
|
|
||||||
@@ -476,8 +550,8 @@ export function getFormStateScript() {
|
|||||||
*/
|
*/
|
||||||
export function navigateSectionScript(name) {
|
export function navigateSectionScript(name) {
|
||||||
return `(() => {
|
return `(() => {
|
||||||
const norm = s => (s?.trim().replace(/\\u00a0/g, ' ') || '').replace(/ё/gi, 'е');
|
const norm = s => (s?.trim().replace(/\\u00a0/g, ' ').replace(/[\\r\\n]+/g, ' ').replace(/ +/g, ' ') || '').replace(/ё/gi, 'е');
|
||||||
const target = ${JSON.stringify(name.toLowerCase().replace(/ё/g, 'е'))};
|
const target = ${JSON.stringify(name.toLowerCase().replace(/ё/g, 'е').replace(/[\r\n]+/g, ' ').replace(/ +/g, ' '))};
|
||||||
const els = [...document.querySelectorAll('[id^="themesCell_theme_"]')];
|
const els = [...document.querySelectorAll('[id^="themesCell_theme_"]')];
|
||||||
let bestEl = els.find(el => norm(el.innerText).toLowerCase() === target);
|
let bestEl = els.find(el => norm(el.innerText).toLowerCase() === target);
|
||||||
if (!bestEl) bestEl = els.find(el => norm(el.innerText).toLowerCase().includes(target));
|
if (!bestEl) bestEl = els.find(el => norm(el.innerText).toLowerCase().includes(target));
|
||||||
@@ -507,12 +581,14 @@ export function openCommandScript(name) {
|
|||||||
* Supports synonym matching: visible text AND internal name from DOM ID.
|
* Supports synonym matching: visible text AND internal name from DOM ID.
|
||||||
* Fuzzy order: exact name -> exact label -> includes name -> includes label.
|
* Fuzzy order: exact name -> exact label -> includes name -> includes label.
|
||||||
*/
|
*/
|
||||||
export function findClickTargetScript(formNum, text) {
|
export function findClickTargetScript(formNum, text, { tableName, gridSelector } = {}) {
|
||||||
const p = `form${formNum}_`;
|
const p = `form${formNum}_`;
|
||||||
return `(() => {
|
return `(() => {
|
||||||
const norm = s => (s?.trim().replace(/\\u00a0/g, ' ') || '').replace(/ё/gi, 'е');
|
const norm = s => (s?.trim().replace(/\\u00a0/g, ' ') || '').replace(/ё/gi, 'е');
|
||||||
const target = ${JSON.stringify(text.toLowerCase().replace(/ё/g, 'е'))};
|
const target = ${JSON.stringify(text.toLowerCase().replace(/ё/g, 'е'))};
|
||||||
const p = ${JSON.stringify(p)};
|
const p = ${JSON.stringify(p)};
|
||||||
|
const tableName = ${JSON.stringify(tableName || '')};
|
||||||
|
const gridSelector = ${JSON.stringify(gridSelector || '')};
|
||||||
const items = [];
|
const items = [];
|
||||||
|
|
||||||
// Buttons (a.press)
|
// Buttons (a.press)
|
||||||
@@ -561,6 +637,39 @@ export function findClickTargetScript(formNum, text) {
|
|||||||
items.push({ id: el.id, name: el.dataset.content, label: '', kind: 'tab' });
|
items.push({ id: el.id, name: el.dataset.content, label: '', kind: 'tab' });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// When table is specified, scope button search to grid's parent container
|
||||||
|
if (gridSelector) {
|
||||||
|
const gridEl = document.querySelector(gridSelector);
|
||||||
|
if (gridEl) {
|
||||||
|
// Find parent container that has id with formPrefix and contains the grid
|
||||||
|
let container = gridEl.parentElement;
|
||||||
|
while (container && container !== document.body) {
|
||||||
|
if (container.id && container.id.startsWith(p)) break;
|
||||||
|
container = container.parentElement;
|
||||||
|
}
|
||||||
|
// Filter items to those inside the container
|
||||||
|
const containerItems = container && container !== document.body
|
||||||
|
? items.filter(i => { const el = document.getElementById(i.id); return el && container.contains(el); })
|
||||||
|
: [];
|
||||||
|
// Try fuzzy match within container first
|
||||||
|
let cf = containerItems.find(i => i.name.toLowerCase() === target);
|
||||||
|
if (!cf) cf = containerItems.find(i => i.label && i.label.toLowerCase() === target);
|
||||||
|
if (!cf) cf = containerItems.find(i => i.name.toLowerCase().includes(target));
|
||||||
|
if (!cf) cf = containerItems.find(i => i.label && i.label.toLowerCase().includes(target));
|
||||||
|
if (cf) return { id: cf.id, kind: cf.kind, name: cf.name };
|
||||||
|
// Fallback: filter by gridName id-prefix (e.g. ИсходящиеКоманднаяПанель_Добавить)
|
||||||
|
const gridName = gridEl.id ? gridEl.id.replace(p, '') : '';
|
||||||
|
if (gridName) {
|
||||||
|
const prefixItems = items.filter(i => i.label && i.label.includes(gridName));
|
||||||
|
let pf = prefixItems.find(i => i.name.toLowerCase() === target);
|
||||||
|
if (!pf) pf = prefixItems.find(i => i.label && i.label.toLowerCase().includes(target));
|
||||||
|
if (!pf) pf = prefixItems.find(i => i.name.toLowerCase().includes(target));
|
||||||
|
if (pf) return { id: pf.id, kind: pf.kind, name: pf.name };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fall through to unscoped search
|
||||||
|
}
|
||||||
|
|
||||||
// Fuzzy match: exact name -> exact label -> startsWith name -> startsWith label -> includes name -> includes label
|
// Fuzzy match: exact name -> exact label -> startsWith name -> startsWith label -> includes name -> includes label
|
||||||
let found = items.find(i => i.name.toLowerCase() === target);
|
let found = items.find(i => i.name.toLowerCase() === target);
|
||||||
if (!found) found = items.find(i => i.label && i.label.toLowerCase() === target);
|
if (!found) found = items.find(i => i.label && i.label.toLowerCase() === target);
|
||||||
|
|||||||
Reference in New Issue
Block a user