mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-06-12 08:54:57 +03:00
refactor(web-test): извлечены DOM-скрипты filter.mjs в dom/filter.mjs
Все 17 inline page.evaluate в engine/table/filter.mjs вынесены в
именованные dom-генераторы. Engine-модуль стал чистым orchestrator-ом.
Новое в dom/forms.mjs (shared с будущим S3 select-value):
- findSearchInputScript(formNum) — поиск SearchString/ПоискаСтроки input
- findNamedButtonScript(text) — кнопка a.press по innerText (Найти, OK)
- findCompareTypeRadioScript(form, idx) — радио CompareType#N#radio
- isFormVisibleScript(form) — есть ли видимые элементы form{N}
Новое в dom/filter.mjs:
- findFirstGridCellCoordsScript — координаты первой клетки грида
- findColumnFirstCellCoordsScript — клетка по имени колонки (fuzzy header
match с needDlb-fallback)
- readFieldSelectorInfoScript — FieldSelector value + DLB coords
- pickFieldInSelectorDropdownScript — выбор поля в FieldSelector DLB-edd
- readFilterDialogInfoScript — Pattern id+value+isDate+isRef
- findFilterBadgeCloseScript — × badge по имени поля
- findFirstFilterBadgeCloseScript — × первого видимого badge (для clear-all)
Попутно: добавлен импорт readSubmenuScript (был pre-existing broken
import в Еще-fallback ветке Alt+F).
Метрики filter.mjs: 390 → 256 LOC (−134, −34%), inline page.evaluate
17 → 0. Регресс 09-filter / 02-crud / 05-table — зелёный.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
// web-test dom v1.9 — facade re-exporting injectable DOM scripts from dom/
|
||||
// web-test dom v1.10 — facade re-exporting injectable DOM scripts from dom/
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
/**
|
||||
* Facade: re-exports DOM selector & semantic mapping script generators.
|
||||
@@ -16,8 +16,22 @@ export {
|
||||
findFieldButtonScript,
|
||||
resolveFieldsScript,
|
||||
detectNewFormScript,
|
||||
findSearchInputScript,
|
||||
findNamedButtonScript,
|
||||
findCompareTypeRadioScript,
|
||||
isFormVisibleScript,
|
||||
} from './dom/forms.mjs';
|
||||
|
||||
export {
|
||||
findFirstGridCellCoordsScript,
|
||||
findColumnFirstCellCoordsScript,
|
||||
readFieldSelectorInfoScript,
|
||||
pickFieldInSelectorDropdownScript,
|
||||
readFilterDialogInfoScript,
|
||||
findFilterBadgeCloseScript,
|
||||
findFirstFilterBadgeCloseScript,
|
||||
} from './dom/filter.mjs';
|
||||
|
||||
export {
|
||||
isInputFocusedScript,
|
||||
isInputFocusedInGridScript,
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
// web-test dom/filter v1.0 — DOM scripts for filterList / unfilterList
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
/**
|
||||
* Find the first grid cell on the form and return its center coords.
|
||||
* Used as a fallback target for Alt+F when there's no search input.
|
||||
*
|
||||
* Returns `{ x, y } | null`.
|
||||
*/
|
||||
export function findFirstGridCellCoordsScript(formNum) {
|
||||
return `(() => {
|
||||
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) };
|
||||
})()`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the grid cell of the first row in the column whose header text matches `field`
|
||||
* (fuzzy: exact → startsWith → includes; normalizes ё/е and NBSP).
|
||||
*
|
||||
* If the column isn't in the grid, returns coords of the first cell + `needDlb: true`
|
||||
* so the caller can use DLB to switch FieldSelector after opening the dialog.
|
||||
*
|
||||
* Returns:
|
||||
* - `{ x, y, needDlb? } ` — coords to click (advanced search target)
|
||||
* - `{ error }` — `'no_grid' | 'no_rows' | 'no_cells' | 'cell_not_found'`
|
||||
*/
|
||||
export function findColumnFirstCellCoordsScript(formNum, field) {
|
||||
return `(() => {
|
||||
const p = 'form${formNum}_';
|
||||
const grid = [...document.querySelectorAll('[id^="' + p + '"].grid, [id^="' + p + '"] .grid')]
|
||||
.find(g => g.offsetWidth > 0);
|
||||
if (!grid) return { error: 'no_grid' };
|
||||
const targetField = ${JSON.stringify(field)};
|
||||
const headers = [...grid.querySelectorAll('.gridHead .gridBox')];
|
||||
let colIndex = -1;
|
||||
let startsWithIdx = -1;
|
||||
let includesIdx = -1;
|
||||
for (let i = 0; i < headers.length; i++) {
|
||||
const t = headers[i].innerText?.trim().replace(/\\u00a0/g, ' ');
|
||||
if (!t) continue;
|
||||
const ny = s => s.replace(/ё/gi, 'е').replace(/\\u00a0/g, ' ');
|
||||
const tl = ny(t.toLowerCase()), fl = ny(targetField.toLowerCase());
|
||||
if (tl === fl) { colIndex = i; break; }
|
||||
if (startsWithIdx < 0 && tl.startsWith(fl)) { startsWithIdx = i; }
|
||||
else if (includesIdx < 0 && tl.includes(fl)) { includesIdx = i; }
|
||||
}
|
||||
if (colIndex < 0) colIndex = startsWithIdx >= 0 ? startsWithIdx : includesIdx;
|
||||
const rows = [...grid.querySelectorAll('.gridBody .gridLine')];
|
||||
if (!rows.length) return { error: 'no_rows' };
|
||||
if (colIndex < 0) {
|
||||
const cells = [...rows[0].querySelectorAll('.gridBox')];
|
||||
if (!cells.length) return { error: 'no_cells' };
|
||||
const r = cells[0].getBoundingClientRect();
|
||||
return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2), needDlb: true };
|
||||
}
|
||||
const cells = [...rows[0].querySelectorAll('.gridBox')];
|
||||
if (colIndex >= cells.length) return { error: 'cell_not_found' };
|
||||
const r = cells[colIndex].getBoundingClientRect();
|
||||
return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) };
|
||||
})()`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read FieldSelector input + its DLB button coords on the advanced search dialog.
|
||||
* Returns `{ current, dlbX, dlbY }` (zero coords if DLB not visible).
|
||||
*/
|
||||
export function readFieldSelectorInfoScript(dialogForm) {
|
||||
return `(() => {
|
||||
const p = 'form' + ${JSON.stringify(String(dialogForm))} + '_';
|
||||
const fsInput = [...document.querySelectorAll('input.editInput[id^="' + p + '"]')]
|
||||
.find(el => el.offsetWidth > 0 && /FieldSelector/i.test(el.id));
|
||||
const dlb = document.getElementById(p + 'FieldSelector_DLB');
|
||||
return {
|
||||
current: fsInput?.value?.trim() || '',
|
||||
dlbX: dlb && dlb.offsetWidth > 0 ? Math.round(dlb.getBoundingClientRect().x + dlb.getBoundingClientRect().width / 2) : 0,
|
||||
dlbY: dlb && dlb.offsetWidth > 0 ? Math.round(dlb.getBoundingClientRect().y + dlb.getBoundingClientRect().height / 2) : 0
|
||||
};
|
||||
})()`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick a field name in the FieldSelector EDD dropdown (fuzzy: exact → includes,
|
||||
* normalizes ё/е and NBSP).
|
||||
*
|
||||
* Returns:
|
||||
* - `{ x, y, name }` — coords + matched name to click
|
||||
* - `{ error, available? }` — `'no_dropdown'` or `'field_not_found'` with list of available names
|
||||
*/
|
||||
export function pickFieldInSelectorDropdownScript(field) {
|
||||
return `(() => {
|
||||
const edd = document.getElementById('editDropDown');
|
||||
if (!edd || edd.offsetWidth === 0) return { error: 'no_dropdown' };
|
||||
const ny = s => s.replace(/ё/gi, 'е').replace(/\\u00a0/g, ' ');
|
||||
const target = ny(${JSON.stringify(field.toLowerCase())});
|
||||
const items = [...edd.querySelectorAll('div')].filter(el =>
|
||||
el.offsetWidth > 0 && el.innerText?.trim() && !el.innerText.includes('\\n'));
|
||||
const match = items.find(el => ny(el.innerText.trim().toLowerCase()) === target)
|
||||
|| items.find(el => ny(el.innerText.trim().toLowerCase()).includes(target));
|
||||
if (!match) return { error: 'field_not_found', available: items.map(el => el.innerText.trim()) };
|
||||
const r = match.getBoundingClientRect();
|
||||
return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2), name: match.innerText.trim() };
|
||||
})()`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read advanced search dialog state — FieldSelector value, Pattern input id+value,
|
||||
* and field type flags (isDate via iCalendB button, isRef via iDLB button on Pattern).
|
||||
*
|
||||
* Returns `{ fieldSelector, patternValue, patternId, isDate, isRef }`.
|
||||
*/
|
||||
export function readFilterDialogInfoScript(dialogForm) {
|
||||
return `(() => {
|
||||
const p = 'form' + ${JSON.stringify(String(dialogForm))} + '_';
|
||||
const fsInput = [...document.querySelectorAll('input.editInput[id^="' + p + '"]')]
|
||||
.find(el => el.offsetWidth > 0 && /FieldSelector/i.test(el.id));
|
||||
const ptInput = [...document.querySelectorAll('input.editInput[id^="' + p + '"]')]
|
||||
.find(el => el.offsetWidth > 0 && /Pattern/i.test(el.id));
|
||||
const ptLabel = ptInput?.closest('label');
|
||||
const btns = ptLabel ? [...ptLabel.querySelectorAll('span.btn')].map(b => b.className) : [];
|
||||
const isDate = btns.some(c => c.includes('iCalendB'));
|
||||
const isRef = !isDate && btns.some(c => c.includes('iDLB'));
|
||||
return {
|
||||
fieldSelector: fsInput?.value?.trim() || '',
|
||||
patternValue: ptInput?.value?.trim() || '',
|
||||
patternId: ptInput?.id || '',
|
||||
isDate,
|
||||
isRef
|
||||
};
|
||||
})()`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the × close button on the filter badge whose title matches `field`
|
||||
* (exact → includes; normalizes ё/е and NBSP).
|
||||
*
|
||||
* Returns:
|
||||
* - `{ x, y, field }` — coords + actual field title from the badge
|
||||
* - `{ error, available }` — `'not_found'` with list of available badge titles
|
||||
*/
|
||||
export function findFilterBadgeCloseScript(formNum, field) {
|
||||
return `(() => {
|
||||
const p = 'form${formNum}_';
|
||||
const norm = s => s?.trim().replace(/\\u00a0/g, ' ').replace(/:$/, '').replace(/\\n/g, ' ') || '';
|
||||
const ny = s => s.replace(/ё/gi, 'е').replace(/\\u00a0/g, ' ');
|
||||
const target = ny(${JSON.stringify(field.toLowerCase())});
|
||||
const items = [...document.querySelectorAll('[id^="' + p + '"].trainItem')].filter(el => el.offsetWidth > 0);
|
||||
for (const item of items) {
|
||||
const titleEl = item.querySelector('.trainName');
|
||||
const title = ny(norm(titleEl?.innerText).toLowerCase());
|
||||
if (title === target || title.includes(target)) {
|
||||
const close = item.querySelector('.trainClose');
|
||||
if (close) {
|
||||
const r = close.getBoundingClientRect();
|
||||
return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2), field: norm(titleEl?.innerText) };
|
||||
}
|
||||
}
|
||||
}
|
||||
const available = items.map(item => norm(item.querySelector('.trainName')?.innerText));
|
||||
return { error: 'not_found', available };
|
||||
})()`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the × close button on the FIRST visible filter badge (for clear-all loop).
|
||||
* Returns `{ x, y } | null`.
|
||||
*/
|
||||
export function findFirstFilterBadgeCloseScript(formNum) {
|
||||
return `(() => {
|
||||
const p = 'form${formNum}_';
|
||||
const item = [...document.querySelectorAll('[id^="' + p + '"].trainItem')]
|
||||
.find(el => el.offsetWidth > 0);
|
||||
if (!item) return null;
|
||||
const close = item.querySelector('.trainClose');
|
||||
if (!close) return null;
|
||||
const r = close.getBoundingClientRect();
|
||||
return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) };
|
||||
})()`;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// web-test dom/forms v1.1 — form detection, content read, click-target/field-button resolution
|
||||
// web-test dom/forms v1.2 — form detection, content read, click-target/field-button resolution
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
import { DETECT_FORM_FN, READ_FORM_FN } from './_shared.mjs';
|
||||
|
||||
@@ -422,3 +422,64 @@ export function detectNewFormScript(prevFormNum, { strict = false } = {}) {
|
||||
return nums.length > 0 ? Math.max(...nums) : null;
|
||||
})()`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the search input on a list form (matches `SearchString` / `ПоискаСтроки` id).
|
||||
* Returns `{ id, value } | null`.
|
||||
*/
|
||||
export function findSearchInputScript(formNum) {
|
||||
return `(() => {
|
||||
const p = 'form${formNum}_';
|
||||
const el = [...document.querySelectorAll('input.editInput[id^="' + p + '"]')]
|
||||
.find(el => el.offsetWidth > 0 && /Строк[аи]Поиска|SearchString/i.test(el.id));
|
||||
return el ? { id: el.id, value: el.value || '' } : null;
|
||||
})()`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a visible `a.press` button by its exact innerText (after trim).
|
||||
* Returns `{ x, y } | null` for `page.mouse.click(x, y)`.
|
||||
*
|
||||
* Used for modal dialog buttons (Найти, OK) where page.click may be blocked.
|
||||
*/
|
||||
export function findNamedButtonScript(buttonText) {
|
||||
return `(() => {
|
||||
const btns = [...document.querySelectorAll('a.press')].filter(el => el.offsetWidth > 0);
|
||||
const btn = btns.find(el => el.innerText?.trim() === ${JSON.stringify(buttonText)});
|
||||
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) };
|
||||
})()`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a CompareType radio button by index (1 = "contains", 2 = "exact", etc.)
|
||||
* on a search/filter dialog.
|
||||
*
|
||||
* Returns:
|
||||
* - `{ already: true }` — the group is disabled OR the radio is already selected
|
||||
* - `{ x, y } | null` — coords to click, or null if radio not present
|
||||
*/
|
||||
export function findCompareTypeRadioScript(dialogForm, radioIndex) {
|
||||
return `(() => {
|
||||
const p = 'form' + ${JSON.stringify(String(dialogForm))} + '_';
|
||||
const group = document.getElementById(p + 'CompareType');
|
||||
if (group && group.classList.contains('disabled')) return { already: true };
|
||||
const el = document.getElementById(p + 'CompareType#' + ${JSON.stringify(String(radioIndex))} + '#radio');
|
||||
if (!el || el.offsetWidth === 0) return null;
|
||||
if (el.classList.contains('select')) return { already: true };
|
||||
const r = el.getBoundingClientRect();
|
||||
return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) };
|
||||
})()`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is any element of `form{dialogForm}_` currently visible?
|
||||
* Used to poll dialog dismissal after Escape.
|
||||
*/
|
||||
export function isFormVisibleScript(dialogForm) {
|
||||
return `(() => {
|
||||
const p = 'form${dialogForm}_';
|
||||
return [...document.querySelectorAll('[id^="' + p + '"]')].some(el => el.offsetWidth > 0);
|
||||
})()`;
|
||||
}
|
||||
|
||||
@@ -1,390 +1,256 @@
|
||||
// web-test table/filter v1.17 — filterList / unfilterList — simple search + advanced-column filter badges.
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import { page, ensureConnected, normYo, highlightMode, ACTION_WAIT } from '../core/state.mjs';
|
||||
import { detectFormScript, resolveGridScript } from '../../dom.mjs';
|
||||
import { dismissPendingErrors, checkForErrors } from '../core/errors.mjs';
|
||||
import { waitForStable, waitForCondition } from '../core/wait.mjs';
|
||||
import { highlight, unhighlight } from '../recording/highlight.mjs';
|
||||
import { safeClick } from '../core/helpers.mjs';
|
||||
import { selectValue, fillReferenceField } from '../forms/select-value.mjs';
|
||||
import { pasteText } from '../core/clipboard.mjs';
|
||||
import { getFormState } from '../forms/state.mjs';
|
||||
import { clickElement } from '../core/click.mjs';
|
||||
|
||||
/**
|
||||
* Filter the current list by field value, or search via search bar.
|
||||
*
|
||||
* Without field: simple search via the search bar (filters by all columns, no badge).
|
||||
* With field: advanced search — clicks target column cell to auto-populate FieldSelector,
|
||||
* opens dialog (Alt+F), fills Pattern, clicks Найти. Creates a real filter badge.
|
||||
* Handles text, reference (with Tab autocomplete), and date fields automatically.
|
||||
* Multiple filters can be chained by calling filterList multiple times.
|
||||
*
|
||||
* @param {string} text - Search text or date (e.g. "Мишка", "КП00", "10.03.2016")
|
||||
* @param {object} [opts]
|
||||
* @param {string} [opts.field] - Column name for advanced search (e.g. "Наименование", "Получатель", "Дата")
|
||||
* @param {boolean} [opts.exact] - Exact match (text fields only; dates/numbers/refs always exact)
|
||||
*/
|
||||
export async function filterList(text, { field, exact } = {}) {
|
||||
ensureConnected();
|
||||
await dismissPendingErrors();
|
||||
const formNum = await page.evaluate(detectFormScript());
|
||||
if (formNum === null) throw new Error('filterList: no form found');
|
||||
|
||||
if (!field) {
|
||||
// --- Simple search: fill search input + Enter ---
|
||||
const searchId = await page.evaluate(`(() => {
|
||||
const p = 'form${formNum}_';
|
||||
const el = [...document.querySelectorAll('input.editInput[id^="' + p + '"]')]
|
||||
.find(el => el.offsetWidth > 0 && /Строк[аи]Поиска|SearchString/i.test(el.id));
|
||||
return el ? el.id : null;
|
||||
})()`);
|
||||
|
||||
if (searchId) {
|
||||
await page.click(`[id="${searchId}"]`);
|
||||
await page.waitForTimeout(200);
|
||||
await page.keyboard.press('Control+A');
|
||||
await pasteText(text);
|
||||
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);
|
||||
field = ''; // fall through to advanced search, skip DLB (empty field = keep auto-selected)
|
||||
}
|
||||
|
||||
// --- Advanced search: click target column cell → Alt+F → fill Pattern → Найти ---
|
||||
// Clicking a cell in the target column makes it active, so when Alt+F opens the
|
||||
// advanced search dialog, FieldSelector is auto-populated with the correct field name.
|
||||
// This avoids changing FieldSelector programmatically (which can cause errors).
|
||||
const isDateValue = /^\d{2}\.\d{2}\.\d{4}$/.test(text.trim());
|
||||
|
||||
// 1. Click a cell in the target column to activate it (auto-populates FieldSelector).
|
||||
// If the column isn't visible in the grid, click any cell and use DLB fallback later.
|
||||
let needDlb = false;
|
||||
const gridEl = 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 { error: 'no_grid' };
|
||||
const targetField = ${JSON.stringify(field)};
|
||||
const headers = [...grid.querySelectorAll('.gridHead .gridBox')];
|
||||
let colIndex = -1;
|
||||
let startsWithIdx = -1;
|
||||
let includesIdx = -1;
|
||||
for (let i = 0; i < headers.length; i++) {
|
||||
const t = headers[i].innerText?.trim().replace(/\\u00a0/g, ' ');
|
||||
if (!t) continue;
|
||||
const ny = s => s.replace(/ё/gi, 'е').replace(/\\u00a0/g, ' ');
|
||||
const tl = ny(t.toLowerCase()), fl = ny(targetField.toLowerCase());
|
||||
if (tl === fl) { colIndex = i; break; }
|
||||
if (startsWithIdx < 0 && tl.startsWith(fl)) { startsWithIdx = i; }
|
||||
else if (includesIdx < 0 && tl.includes(fl)) { includesIdx = i; }
|
||||
}
|
||||
if (colIndex < 0) colIndex = startsWithIdx >= 0 ? startsWithIdx : includesIdx;
|
||||
const rows = [...grid.querySelectorAll('.gridBody .gridLine')];
|
||||
if (!rows.length) return { error: 'no_rows' };
|
||||
if (colIndex < 0) {
|
||||
// Column not in grid — click first cell of first row, will use DLB to change field
|
||||
const cells = [...rows[0].querySelectorAll('.gridBox')];
|
||||
if (!cells.length) return { error: 'no_cells' };
|
||||
const r = cells[0].getBoundingClientRect();
|
||||
return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2), needDlb: true };
|
||||
}
|
||||
const cells = [...rows[0].querySelectorAll('.gridBox')];
|
||||
if (colIndex >= cells.length) return { error: 'cell_not_found' };
|
||||
const r = cells[colIndex].getBoundingClientRect();
|
||||
return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) };
|
||||
})()`);
|
||||
if (gridEl.error) throw new Error(`filterList: ${gridEl.error}`);
|
||||
needDlb = !!gridEl.needDlb;
|
||||
await page.mouse.click(gridEl.x, gridEl.y);
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// 2. Open advanced search dialog via Alt+F (with fallback to Еще menu)
|
||||
await page.keyboard.press('Alt+f');
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
let dialogForm = await page.evaluate(detectFormScript());
|
||||
if (dialogForm === formNum) {
|
||||
// Alt+F didn't open dialog — fallback to Еще → Расширенный поиск
|
||||
await clickElement('Еще');
|
||||
await page.waitForTimeout(500);
|
||||
const menu = await page.evaluate(readSubmenuScript());
|
||||
const searchItem = Array.isArray(menu) && menu.find(i =>
|
||||
i.name.replace(/\u00a0/g, ' ').toLowerCase().includes('расширенный поиск'));
|
||||
if (!searchItem) {
|
||||
await page.keyboard.press('Escape');
|
||||
throw new Error('filterList: advanced search dialog could not be opened');
|
||||
}
|
||||
await page.mouse.click(searchItem.x, searchItem.y);
|
||||
await page.waitForTimeout(2000);
|
||||
dialogForm = await page.evaluate(detectFormScript());
|
||||
if (dialogForm === formNum) {
|
||||
throw new Error('filterList: advanced search dialog did not open');
|
||||
}
|
||||
}
|
||||
|
||||
// 2b. If column wasn't in the grid, change FieldSelector via DLB dropdown
|
||||
// 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 p = 'form' + ${JSON.stringify(String(dialogForm))} + '_';
|
||||
const fsInput = [...document.querySelectorAll('input.editInput[id^="' + p + '"]')]
|
||||
.find(el => el.offsetWidth > 0 && /FieldSelector/i.test(el.id));
|
||||
const dlb = document.getElementById(p + 'FieldSelector_DLB');
|
||||
return {
|
||||
current: fsInput?.value?.trim() || '',
|
||||
dlbX: dlb && dlb.offsetWidth > 0 ? Math.round(dlb.getBoundingClientRect().x + dlb.getBoundingClientRect().width / 2) : 0,
|
||||
dlbY: dlb && dlb.offsetWidth > 0 ? Math.round(dlb.getBoundingClientRect().y + dlb.getBoundingClientRect().height / 2) : 0
|
||||
};
|
||||
})()`);
|
||||
|
||||
if (normYo(fsInfo.current.toLowerCase()) !== normYo(field.toLowerCase())) {
|
||||
await page.mouse.click(fsInfo.dlbX, fsInfo.dlbY);
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
const ddResult = await page.evaluate(`(() => {
|
||||
const edd = document.getElementById('editDropDown');
|
||||
if (!edd || edd.offsetWidth === 0) return { error: 'no_dropdown' };
|
||||
const ny = s => s.replace(/ё/gi, 'е').replace(/\\u00a0/g, ' ');
|
||||
const target = ny(${JSON.stringify(field.toLowerCase())});
|
||||
const items = [...edd.querySelectorAll('div')].filter(el =>
|
||||
el.offsetWidth > 0 && el.innerText?.trim() && !el.innerText.includes('\\n'));
|
||||
const match = items.find(el => ny(el.innerText.trim().toLowerCase()) === target)
|
||||
|| items.find(el => ny(el.innerText.trim().toLowerCase()).includes(target));
|
||||
if (!match) return { error: 'field_not_found', available: items.map(el => el.innerText.trim()) };
|
||||
const r = match.getBoundingClientRect();
|
||||
return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2), name: match.innerText.trim() };
|
||||
})()`);
|
||||
|
||||
if (ddResult.error) {
|
||||
await page.keyboard.press('Escape');
|
||||
await page.waitForTimeout(500);
|
||||
await page.keyboard.press('Escape');
|
||||
await page.waitForTimeout(500);
|
||||
throw new Error(`filterList: field "${field}" not found in FieldSelector. Available: ${ddResult.available?.join(', ') || 'none'}`);
|
||||
}
|
||||
await page.mouse.click(ddResult.x, ddResult.y);
|
||||
await page.waitForTimeout(3000);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Read dialog state and fill Pattern
|
||||
// Detect field type by Pattern's sibling buttons:
|
||||
// - iCalendB → date field (Home+Shift+End+Ctrl+V to replace date value)
|
||||
// - iDLB on Pattern → reference field (paste + Tab for autocomplete)
|
||||
// - neither → plain text field (just paste)
|
||||
const dialogInfo = await page.evaluate(`(() => {
|
||||
const p = 'form' + ${JSON.stringify(String(dialogForm))} + '_';
|
||||
const fsInput = [...document.querySelectorAll('input.editInput[id^="' + p + '"]')]
|
||||
.find(el => el.offsetWidth > 0 && /FieldSelector/i.test(el.id));
|
||||
const ptInput = [...document.querySelectorAll('input.editInput[id^="' + p + '"]')]
|
||||
.find(el => el.offsetWidth > 0 && /Pattern/i.test(el.id));
|
||||
const ptLabel = ptInput?.closest('label');
|
||||
const btns = ptLabel ? [...ptLabel.querySelectorAll('span.btn')].map(b => b.className) : [];
|
||||
const isDate = btns.some(c => c.includes('iCalendB'));
|
||||
const isRef = !isDate && btns.some(c => c.includes('iDLB'));
|
||||
return {
|
||||
fieldSelector: fsInput?.value?.trim() || '',
|
||||
patternValue: ptInput?.value?.trim() || '',
|
||||
patternId: ptInput?.id || '',
|
||||
isDate,
|
||||
isRef
|
||||
};
|
||||
})()`);
|
||||
|
||||
if (dialogInfo.isDate) {
|
||||
// Date field: fill via Home → Shift+End (select all) → Ctrl+V (paste)
|
||||
if (isDateValue && dialogInfo.patternValue !== text.trim()) {
|
||||
await page.click(`[id="${dialogInfo.patternId}"]`);
|
||||
await page.waitForTimeout(200);
|
||||
await page.keyboard.press('Home');
|
||||
await page.waitForTimeout(100);
|
||||
await page.keyboard.press('Shift+End');
|
||||
await page.waitForTimeout(100);
|
||||
await pasteText(text);
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
} else {
|
||||
// Text or reference field: fill Pattern via clipboard paste
|
||||
await page.click(`[id="${dialogInfo.patternId}"]`);
|
||||
await page.waitForTimeout(200);
|
||||
await page.keyboard.press('Control+A');
|
||||
await pasteText(text);
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
if (dialogInfo.isRef) {
|
||||
// Reference field: Tab triggers autocomplete to resolve text → reference value
|
||||
await page.keyboard.press('Tab');
|
||||
await page.waitForTimeout(2000);
|
||||
}
|
||||
}
|
||||
|
||||
// 3b. Switch CompareType if exact match requested (text fields only).
|
||||
// Date/number: always exact, CompareType disabled. Reference: default exact (selects ref).
|
||||
if (exact && !dialogInfo.isDate && !dialogInfo.isRef) {
|
||||
const exactRadio = await page.evaluate(`(() => {
|
||||
const p = 'form' + ${JSON.stringify(String(dialogForm))} + '_';
|
||||
// Check if CompareType group is disabled (dates, numbers)
|
||||
const group = document.getElementById(p + 'CompareType');
|
||||
if (group && group.classList.contains('disabled')) return { already: true };
|
||||
const el = document.getElementById(p + 'CompareType#2#radio');
|
||||
if (!el || el.offsetWidth === 0) return null;
|
||||
if (el.classList.contains('select')) return { already: true };
|
||||
const r = el.getBoundingClientRect();
|
||||
return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) };
|
||||
})()`);
|
||||
if (exactRadio && !exactRadio.already) {
|
||||
await page.mouse.click(exactRadio.x, exactRadio.y);
|
||||
await page.waitForTimeout(300);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Click "Найти" via mouse.click (dialog is modal — page.click may be blocked)
|
||||
const findBtnCoords = 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 (findBtnCoords) {
|
||||
await page.mouse.click(findBtnCoords.x, findBtnCoords.y);
|
||||
} else {
|
||||
await clickElement('Найти');
|
||||
}
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// 5. Close advanced search dialog if it stayed open (some forms keep it open after Найти).
|
||||
// Check the specific dialog form — not generic modalSurface — to avoid closing parent modals
|
||||
// (e.g. a selection form that opened this advanced search).
|
||||
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);
|
||||
|
||||
const state = await getFormState();
|
||||
state.filtered = { type: 'advanced', field, text, exact: !!exact };
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove active filters/search from the current list.
|
||||
*
|
||||
* Without field: clears ALL filters (Ctrl+Q for advanced search + clear search field).
|
||||
* With field: clicks the × button on the specific filter badge (selective removal).
|
||||
*
|
||||
* @param {object} [opts]
|
||||
* @param {string} [opts.field] - Remove only the filter for this field (clicks badge ×)
|
||||
*/
|
||||
export async function unfilterList({ field } = {}) {
|
||||
ensureConnected();
|
||||
await dismissPendingErrors();
|
||||
const formNum = await page.evaluate(detectFormScript());
|
||||
if (formNum === null) throw new Error('unfilterList: no form found');
|
||||
|
||||
if (field) {
|
||||
// --- Selective: click × on specific filter badge ---
|
||||
const closeBtn = await page.evaluate(`(() => {
|
||||
const p = 'form${formNum}_';
|
||||
const norm = s => s?.trim().replace(/\\u00a0/g, ' ').replace(/:$/, '').replace(/\\n/g, ' ') || '';
|
||||
const ny = s => s.replace(/ё/gi, 'е').replace(/\\u00a0/g, ' ');
|
||||
const target = ny(${JSON.stringify(field.toLowerCase())});
|
||||
const items = [...document.querySelectorAll('[id^="' + p + '"].trainItem')].filter(el => el.offsetWidth > 0);
|
||||
for (const item of items) {
|
||||
const titleEl = item.querySelector('.trainName');
|
||||
const title = ny(norm(titleEl?.innerText).toLowerCase());
|
||||
if (title === target || title.includes(target)) {
|
||||
const close = item.querySelector('.trainClose');
|
||||
if (close) {
|
||||
const r = close.getBoundingClientRect();
|
||||
return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2), field: norm(titleEl?.innerText) };
|
||||
}
|
||||
}
|
||||
}
|
||||
const available = items.map(item => norm(item.querySelector('.trainName')?.innerText));
|
||||
return { error: 'not_found', available };
|
||||
})()`);
|
||||
|
||||
if (closeBtn?.error) throw new Error(`unfilterList: filter badge "${field}" not found. Available: ${closeBtn.available?.join(', ') || 'none'}`);
|
||||
await page.mouse.click(closeBtn.x, closeBtn.y);
|
||||
await waitForStable(formNum);
|
||||
|
||||
const state = await getFormState();
|
||||
state.unfiltered = { field: closeBtn.field };
|
||||
return state;
|
||||
}
|
||||
|
||||
// --- Clear ALL filters ---
|
||||
|
||||
// 1. Remove all advanced filter badges (.trainItem × buttons)
|
||||
for (let attempt = 0; attempt < 20; attempt++) {
|
||||
const badge = await page.evaluate(`(() => {
|
||||
const p = 'form${formNum}_';
|
||||
const item = [...document.querySelectorAll('[id^="' + p + '"].trainItem')]
|
||||
.find(el => el.offsetWidth > 0);
|
||||
if (!item) return null;
|
||||
const close = item.querySelector('.trainClose');
|
||||
if (!close) return null;
|
||||
const r = close.getBoundingClientRect();
|
||||
return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) };
|
||||
})()`);
|
||||
if (!badge) break;
|
||||
await page.mouse.click(badge.x, badge.y);
|
||||
await waitForStable(formNum);
|
||||
}
|
||||
|
||||
// 2. Cancel active search via Ctrl+Q
|
||||
await page.keyboard.press('Control+q');
|
||||
await waitForStable(formNum);
|
||||
|
||||
// 3. Clear simple search field if it has a value
|
||||
const searchInfo = await page.evaluate(`(() => {
|
||||
const p = 'form${formNum}_';
|
||||
const el = [...document.querySelectorAll('input.editInput[id^="' + p + '"]')]
|
||||
.find(el => el.offsetWidth > 0 && /Строк[аи]Поиска|SearchString/i.test(el.id));
|
||||
return el ? { id: el.id, value: el.value || '' } : null;
|
||||
})()`);
|
||||
|
||||
if (searchInfo?.value) {
|
||||
await page.click(`[id="${searchInfo.id}"]`);
|
||||
await page.waitForTimeout(200);
|
||||
await page.keyboard.press('Control+A');
|
||||
await page.keyboard.press('Delete');
|
||||
await page.keyboard.press('Enter');
|
||||
await waitForStable(formNum);
|
||||
}
|
||||
|
||||
const state = await getFormState();
|
||||
state.unfiltered = true;
|
||||
return state;
|
||||
}
|
||||
// web-test table/filter v1.18 — filterList / unfilterList — simple search + advanced-column filter badges.
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import { page, ensureConnected, normYo, highlightMode, ACTION_WAIT } from '../core/state.mjs';
|
||||
import {
|
||||
detectFormScript, readSubmenuScript,
|
||||
findSearchInputScript, findNamedButtonScript, findCompareTypeRadioScript, isFormVisibleScript,
|
||||
findFirstGridCellCoordsScript, findColumnFirstCellCoordsScript,
|
||||
readFieldSelectorInfoScript, pickFieldInSelectorDropdownScript,
|
||||
readFilterDialogInfoScript, findFilterBadgeCloseScript, findFirstFilterBadgeCloseScript,
|
||||
} from '../../dom.mjs';
|
||||
import { dismissPendingErrors, checkForErrors } from '../core/errors.mjs';
|
||||
import { waitForStable, waitForCondition } from '../core/wait.mjs';
|
||||
import { highlight, unhighlight } from '../recording/highlight.mjs';
|
||||
import { safeClick } from '../core/helpers.mjs';
|
||||
import { selectValue, fillReferenceField } from '../forms/select-value.mjs';
|
||||
import { pasteText } from '../core/clipboard.mjs';
|
||||
import { getFormState } from '../forms/state.mjs';
|
||||
import { clickElement } from '../core/click.mjs';
|
||||
|
||||
/**
|
||||
* Filter the current list by field value, or search via search bar.
|
||||
*
|
||||
* Without field: simple search via the search bar (filters by all columns, no badge).
|
||||
* With field: advanced search — clicks target column cell to auto-populate FieldSelector,
|
||||
* opens dialog (Alt+F), fills Pattern, clicks Найти. Creates a real filter badge.
|
||||
* Handles text, reference (with Tab autocomplete), and date fields automatically.
|
||||
* Multiple filters can be chained by calling filterList multiple times.
|
||||
*
|
||||
* @param {string} text - Search text or date (e.g. "Мишка", "КП00", "10.03.2016")
|
||||
* @param {object} [opts]
|
||||
* @param {string} [opts.field] - Column name for advanced search (e.g. "Наименование", "Получатель", "Дата")
|
||||
* @param {boolean} [opts.exact] - Exact match (text fields only; dates/numbers/refs always exact)
|
||||
*/
|
||||
export async function filterList(text, { field, exact } = {}) {
|
||||
ensureConnected();
|
||||
await dismissPendingErrors();
|
||||
const formNum = await page.evaluate(detectFormScript());
|
||||
if (formNum === null) throw new Error('filterList: no form found');
|
||||
|
||||
if (!field) {
|
||||
// --- Simple search: fill search input + Enter ---
|
||||
const searchInfo = await page.evaluate(findSearchInputScript(formNum));
|
||||
|
||||
if (searchInfo) {
|
||||
await page.click(`[id="${searchInfo.id}"]`);
|
||||
await page.waitForTimeout(200);
|
||||
await page.keyboard.press('Control+A');
|
||||
await pasteText(text);
|
||||
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(findFirstGridCellCoordsScript(formNum));
|
||||
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);
|
||||
field = ''; // fall through to advanced search, skip DLB (empty field = keep auto-selected)
|
||||
}
|
||||
|
||||
// --- Advanced search: click target column cell → Alt+F → fill Pattern → Найти ---
|
||||
// Clicking a cell in the target column makes it active, so when Alt+F opens the
|
||||
// advanced search dialog, FieldSelector is auto-populated with the correct field name.
|
||||
// This avoids changing FieldSelector programmatically (which can cause errors).
|
||||
const isDateValue = /^\d{2}\.\d{2}\.\d{4}$/.test(text.trim());
|
||||
|
||||
// 1. Click a cell in the target column to activate it (auto-populates FieldSelector).
|
||||
// If the column isn't visible in the grid, click any cell and use DLB fallback later.
|
||||
let needDlb = false;
|
||||
const gridEl = await page.evaluate(findColumnFirstCellCoordsScript(formNum, field));
|
||||
if (gridEl.error) throw new Error(`filterList: ${gridEl.error}`);
|
||||
needDlb = !!gridEl.needDlb;
|
||||
await page.mouse.click(gridEl.x, gridEl.y);
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// 2. Open advanced search dialog via Alt+F (with fallback to Еще menu)
|
||||
await page.keyboard.press('Alt+f');
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
let dialogForm = await page.evaluate(detectFormScript());
|
||||
if (dialogForm === formNum) {
|
||||
// Alt+F didn't open dialog — fallback to Еще → Расширенный поиск
|
||||
await clickElement('Еще');
|
||||
await page.waitForTimeout(500);
|
||||
const menu = await page.evaluate(readSubmenuScript());
|
||||
const searchItem = Array.isArray(menu) && menu.find(i =>
|
||||
i.name.replace(/ /g, ' ').toLowerCase().includes('расширенный поиск'));
|
||||
if (!searchItem) {
|
||||
await page.keyboard.press('Escape');
|
||||
throw new Error('filterList: advanced search dialog could not be opened');
|
||||
}
|
||||
await page.mouse.click(searchItem.x, searchItem.y);
|
||||
await page.waitForTimeout(2000);
|
||||
dialogForm = await page.evaluate(detectFormScript());
|
||||
if (dialogForm === formNum) {
|
||||
throw new Error('filterList: advanced search dialog did not open');
|
||||
}
|
||||
}
|
||||
|
||||
// 2b. If column wasn't in the grid, change FieldSelector via DLB dropdown
|
||||
// Skip DLB when field is empty (fallback from no-search-input path — keep auto-selected field)
|
||||
if (needDlb && field) {
|
||||
const fsInfo = await page.evaluate(readFieldSelectorInfoScript(dialogForm));
|
||||
|
||||
if (normYo(fsInfo.current.toLowerCase()) !== normYo(field.toLowerCase())) {
|
||||
await page.mouse.click(fsInfo.dlbX, fsInfo.dlbY);
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
const ddResult = await page.evaluate(pickFieldInSelectorDropdownScript(field));
|
||||
|
||||
if (ddResult.error) {
|
||||
await page.keyboard.press('Escape');
|
||||
await page.waitForTimeout(500);
|
||||
await page.keyboard.press('Escape');
|
||||
await page.waitForTimeout(500);
|
||||
throw new Error(`filterList: field "${field}" not found in FieldSelector. Available: ${ddResult.available?.join(', ') || 'none'}`);
|
||||
}
|
||||
await page.mouse.click(ddResult.x, ddResult.y);
|
||||
await page.waitForTimeout(3000);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Read dialog state and fill Pattern
|
||||
// Detect field type by Pattern's sibling buttons:
|
||||
// - iCalendB → date field (Home+Shift+End+Ctrl+V to replace date value)
|
||||
// - iDLB on Pattern → reference field (paste + Tab for autocomplete)
|
||||
// - neither → plain text field (just paste)
|
||||
const dialogInfo = await page.evaluate(readFilterDialogInfoScript(dialogForm));
|
||||
|
||||
if (dialogInfo.isDate) {
|
||||
// Date field: fill via Home → Shift+End (select all) → Ctrl+V (paste)
|
||||
if (isDateValue && dialogInfo.patternValue !== text.trim()) {
|
||||
await page.click(`[id="${dialogInfo.patternId}"]`);
|
||||
await page.waitForTimeout(200);
|
||||
await page.keyboard.press('Home');
|
||||
await page.waitForTimeout(100);
|
||||
await page.keyboard.press('Shift+End');
|
||||
await page.waitForTimeout(100);
|
||||
await pasteText(text);
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
} else {
|
||||
// Text or reference field: fill Pattern via clipboard paste
|
||||
await page.click(`[id="${dialogInfo.patternId}"]`);
|
||||
await page.waitForTimeout(200);
|
||||
await page.keyboard.press('Control+A');
|
||||
await pasteText(text);
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
if (dialogInfo.isRef) {
|
||||
// Reference field: Tab triggers autocomplete to resolve text → reference value
|
||||
await page.keyboard.press('Tab');
|
||||
await page.waitForTimeout(2000);
|
||||
}
|
||||
}
|
||||
|
||||
// 3b. Switch CompareType if exact match requested (text fields only).
|
||||
// Date/number: always exact, CompareType disabled. Reference: default exact (selects ref).
|
||||
if (exact && !dialogInfo.isDate && !dialogInfo.isRef) {
|
||||
const exactRadio = await page.evaluate(findCompareTypeRadioScript(dialogForm, 2));
|
||||
if (exactRadio && !exactRadio.already) {
|
||||
await page.mouse.click(exactRadio.x, exactRadio.y);
|
||||
await page.waitForTimeout(300);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Click "Найти" via mouse.click (dialog is modal — page.click may be blocked)
|
||||
const findBtnCoords = await page.evaluate(findNamedButtonScript('Найти'));
|
||||
if (findBtnCoords) {
|
||||
await page.mouse.click(findBtnCoords.x, findBtnCoords.y);
|
||||
} else {
|
||||
await clickElement('Найти');
|
||||
}
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// 5. Close advanced search dialog if it stayed open (some forms keep it open after Найти).
|
||||
// Check the specific dialog form — not generic modalSurface — to avoid closing parent modals
|
||||
// (e.g. a selection form that opened this advanced search).
|
||||
for (let attempt = 0; attempt < 3; attempt++) {
|
||||
const dialogVisible = await page.evaluate(isFormVisibleScript(dialogForm));
|
||||
if (!dialogVisible) break;
|
||||
await page.keyboard.press('Escape');
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
await waitForStable(formNum);
|
||||
|
||||
const state = await getFormState();
|
||||
state.filtered = { type: 'advanced', field, text, exact: !!exact };
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove active filters/search from the current list.
|
||||
*
|
||||
* Without field: clears ALL filters (Ctrl+Q for advanced search + clear search field).
|
||||
* With field: clicks the × button on the specific filter badge (selective removal).
|
||||
*
|
||||
* @param {object} [opts]
|
||||
* @param {string} [opts.field] - Remove only the filter for this field (clicks badge ×)
|
||||
*/
|
||||
export async function unfilterList({ field } = {}) {
|
||||
ensureConnected();
|
||||
await dismissPendingErrors();
|
||||
const formNum = await page.evaluate(detectFormScript());
|
||||
if (formNum === null) throw new Error('unfilterList: no form found');
|
||||
|
||||
if (field) {
|
||||
// --- Selective: click × on specific filter badge ---
|
||||
const closeBtn = await page.evaluate(findFilterBadgeCloseScript(formNum, field));
|
||||
|
||||
if (closeBtn?.error) throw new Error(`unfilterList: filter badge "${field}" not found. Available: ${closeBtn.available?.join(', ') || 'none'}`);
|
||||
await page.mouse.click(closeBtn.x, closeBtn.y);
|
||||
await waitForStable(formNum);
|
||||
|
||||
const state = await getFormState();
|
||||
state.unfiltered = { field: closeBtn.field };
|
||||
return state;
|
||||
}
|
||||
|
||||
// --- Clear ALL filters ---
|
||||
|
||||
// 1. Remove all advanced filter badges (.trainItem × buttons)
|
||||
for (let attempt = 0; attempt < 20; attempt++) {
|
||||
const badge = await page.evaluate(findFirstFilterBadgeCloseScript(formNum));
|
||||
if (!badge) break;
|
||||
await page.mouse.click(badge.x, badge.y);
|
||||
await waitForStable(formNum);
|
||||
}
|
||||
|
||||
// 2. Cancel active search via Ctrl+Q
|
||||
await page.keyboard.press('Control+q');
|
||||
await waitForStable(formNum);
|
||||
|
||||
// 3. Clear simple search field if it has a value
|
||||
const searchInfo = await page.evaluate(findSearchInputScript(formNum));
|
||||
|
||||
if (searchInfo?.value) {
|
||||
await page.click(`[id="${searchInfo.id}"]`);
|
||||
await page.waitForTimeout(200);
|
||||
await page.keyboard.press('Control+A');
|
||||
await page.keyboard.press('Delete');
|
||||
await page.keyboard.press('Enter');
|
||||
await waitForStable(formNum);
|
||||
}
|
||||
|
||||
const state = await getFormState();
|
||||
state.unfiltered = true;
|
||||
return state;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user