/** * JS раздела «Запросы». Список: копирование/удаление. Редактор: сохранение * (ajax), палитра тегов + проверка PHP (lint) + разворот редактора, условия * (CRUD + drag-and-drop сортировка). Всё через ajax, подтверждения — Adminx.Confirm. */ (function (window, document) { 'use strict'; var Adminx = window.Adminx || (window.Adminx = {}); Adminx.Requests = { activeField: null, dragRow: null, explainSearchTimer: null, init: function () { this.form = document.getElementById('requestForm'); this.condTable = document.getElementById('condTable'); this.palette = document.querySelector('[data-req-tags-panel]'); this.auditPage = document.querySelector('[data-native-audit-page]'); this.auditRunning = false; var self = this; this.initEditorMode(); this.initPresentation(); this.initSortBuilder(); document.addEventListener('click', function (e) { self.onClick(e); }); // активный редактор — тот, в котором фокус document.addEventListener('focusin', function (e) { var f = e.target.closest('[data-req-code-field]'); if (f) { self.activeField = f; } }); // поиск по тегам + фильтр списка запросов document.addEventListener('input', function (e) { if (e.target.matches('[data-req-tags-search]')) { self.filterTags(e.target.value); } if (e.target.matches('[data-requests-search]')) { self.filterList(e.target.value); } if (e.target.matches('[data-request-explain-search]')) { self.explainSearchInput(e.target); } if (e.target.matches('[data-request-sort-seed-input]')) { self.syncSortRules(); } if (e.target.matches('[data-cf="condition_value_key"], [data-cf="condition_value_constant"], [data-cf="condition_value"]')) { self.conditionValueRefresh(e.target.closest('[data-condition-value]')); } if (e.target.matches('[data-condition-smart-input]')) { self.conditionSmartValue(e.target); } }); document.addEventListener('change', function (e) { if (e.target.matches('[data-group-title]')) { self.groupTitle(e.target); } if (e.target.matches('[data-request-preview-renderer-advanced]')) { self.selectPresentation(e.target.value); } if (e.target.matches('[data-request-sort-source]')) { self.sortSourceChanged(e.target); } if (e.target.matches('[data-cf="condition_value_source"], [data-cf="condition_value_mode"], [data-cf="condition_field_id"], [data-cf="condition_compare"]')) { var conditionRow = e.target.closest('tr'); if (e.target.matches('[data-cf="condition_field_id"]')) { self.conditionFieldRefresh(conditionRow, true); } else if (e.target.matches('[data-cf="condition_compare"], [data-cf="condition_value_source"]')) { self.conditionFieldRefresh(conditionRow, false); } self.conditionValueRefresh(conditionRow.querySelector('[data-condition-value]'), e.target); self.syncEditorAdvancedOptions(); } if (e.target.matches('[data-condition-smart-select]')) { self.conditionSmartValue(e.target); } }); // сохранение запроса if (this.form) { this.form.addEventListener('submit', function (e) { e.preventDefault(); self.save(); }); } // drag-and-drop условий if (this.condTable) { this.condTable.querySelectorAll('tr[data-cond-id]').forEach(function (row) { self.conditionFieldRefresh(row, false); }); this.condTable.addEventListener('dragstart', function (e) { self.dragStart(e); }); this.condTable.addEventListener('dragover', function (e) { self.dragOver(e); }); this.condTable.addEventListener('drop', function (e) { self.drop(e); }); this.condTable.addEventListener('dragend', function () { self.dragEnd(); }); } // Esc закрывает fullscreen/палитру document.addEventListener('keydown', function (e) { if (e.key !== 'Escape') { return; } var fs = document.querySelector('.req-code-field.is-fullscreen'); if (fs) { self.fullscreen(fs, false); } else if (self.palette && !self.palette.hidden) { self.closePalette(); } }); }, base: function () { return (this.form && this.form.getAttribute('data-base')) || Adminx.base(); }, initEditorMode: function () { if (!document.querySelector('[data-request-editor-mode]')) { return; } var mode = 'basic'; try { mode = window.localStorage.getItem('adminx.requests.editorMode') === 'advanced' ? 'advanced' : 'basic'; } catch (e) {} this.setEditorMode(mode, false); }, setEditorMode: function (mode, remember) { mode = mode === 'advanced' ? 'advanced' : 'basic'; this.editorMode = mode; document.querySelectorAll('[data-request-advanced]').forEach(function (node) { node.hidden = mode !== 'advanced'; }); document.querySelectorAll('[data-request-editor-mode-value]').forEach(function (button) { var active = button.getAttribute('data-request-editor-mode-value') === mode; button.classList.toggle('is-active', active); button.setAttribute('aria-pressed', active ? 'true' : 'false'); }); var description = document.querySelector('[data-request-editor-mode-description]'); if (description) { description.textContent = mode === 'advanced' ? 'Кеш, интеграции, совместимость, контракт результата и Native executor.' : 'Основные параметры, условия и оформление результата.'; } this.syncEditorAdvancedOptions(); if (mode === 'advanced') { window.requestAnimationFrame(function () { document.querySelectorAll('[data-req-code-field]').forEach(function (field) { var textarea = field.querySelector('textarea[data-code-editor]'); if (textarea && textarea._adminxCodeMirror) { textarea._adminxCodeMirror.refresh(); } }); }); } if (remember !== false) { try { window.localStorage.setItem('adminx.requests.editorMode', mode); } catch (e) {} } }, initPresentation: function () { var select = document.querySelector('[data-request-preview-renderer]'); if (!select) { return; } this.selectPresentation(select.value, false); }, selectPresentation: function (code) { var select = document.querySelector('[data-request-preview-renderer]'); if (!select) { return; } var available = Array.prototype.some.call(select.options, function (option) { return option.value === code; }); if (!available) { code = 'data_cards'; } select.value = code; var advanced = document.querySelector('[data-request-preview-renderer-advanced]'); if (advanced) { advanced.value = code; } document.querySelectorAll('[data-request-presentation-value]').forEach(function (button) { var active = button.getAttribute('data-request-presentation-value') === code; button.classList.toggle('is-active', active); button.setAttribute('aria-pressed', active ? 'true' : 'false'); }); }, openCodeEditor: function () { this.setEditorMode('advanced'); var field = document.getElementById('requestSiteTemplates'); if (!field) { return; } window.requestAnimationFrame(function () { field.scrollIntoView({behavior: 'smooth', block: 'start'}); var textarea = field.querySelector('textarea[data-code-editor]'); if (textarea && textarea._adminxCodeMirror) { textarea._adminxCodeMirror.focus(); } else if (textarea) { textarea.focus(); } }); }, initSortBuilder: function () { this.sortList = document.querySelector('[data-request-sort-list]'); if (!this.sortList) { return; } var self = this; this.sortList.querySelectorAll('[data-request-sort-source]').forEach(function (select) { select.setAttribute('data-sort-source-value', select.value); }); this.sortList.addEventListener('dragstart', function (event) { self.sortDragStart(event); }); this.sortList.addEventListener('dragover', function (event) { self.sortDragOver(event); }); this.sortList.addEventListener('drop', function (event) { self.sortDrop(event); }); this.sortList.addEventListener('dragend', function () { self.sortDragEnd(); }); this.syncSortRules(); }, sortAdd: function (preferred) { if (!this.sortList) { return; } var rows = this.sortList.querySelectorAll('[data-request-sort-rule]'); if (rows.length >= 8) { Adminx.Toast.show('Можно добавить не больше восьми уровней', 'warning'); return; } var randomSelected = Array.from(this.sortList.querySelectorAll('[data-request-sort-source]')).some(function (select) { return select.value === 'random:RAND()'; }); if (randomSelected) { Adminx.Toast.show('Случайный порядок используется без других уровней', 'warning'); return; } var template = document.querySelector('[data-request-sort-template]'); if (!template) { return; } var row = template.content.firstElementChild.cloneNode(true); var select = row.querySelector('[data-request-sort-source]'); var used = {}; this.sortList.querySelectorAll('[data-request-sort-source]').forEach(function (current) { used[current.value] = true; }); var target = preferred || ''; if (!target || used[target]) { var option = Array.from(select.options).find(function (candidate) { return candidate.value !== 'random:RAND()' && !used[candidate.value]; }); target = option ? option.value : ''; } if (!target) { Adminx.Toast.show('Все доступные поля уже добавлены', 'warning'); return; } select.value = target; select.setAttribute('data-sort-source-value', target); if (preferred === 'document:Id') { row.querySelectorAll('[data-request-sort-direction]').forEach(function (button) { var active = button.getAttribute('data-request-sort-direction') === 'ASC'; button.classList.toggle('is-active', active); button.setAttribute('aria-pressed', active ? 'true' : 'false'); }); } this.sortList.appendChild(row); this.syncSortRules(); }, sortSourceChanged: function (select) { var previous = select.getAttribute('data-sort-source-value') || ''; var values = Array.from(this.sortList.querySelectorAll('[data-request-sort-source]')).filter(function (current) { return current !== select; }).map(function (current) { return current.value; }); if (values.indexOf(select.value) !== -1) { select.value = previous; Adminx.Toast.show('Это поле уже участвует в сортировке', 'warning'); return; } if ((select.value === 'random:RAND()' && values.length) || (select.value !== 'random:RAND()' && values.indexOf('random:RAND()') !== -1)) { select.value = previous; Adminx.Toast.show('Случайный порядок нельзя сочетать с другими уровнями', 'warning'); return; } select.setAttribute('data-sort-source-value', select.value); var row = select.closest('[data-request-sort-rule]'); if (row) { row.classList.toggle('is-random', select.value === 'random:RAND()'); } this.syncSortRules(); if (row && select.value === 'random:RAND()') { window.requestAnimationFrame(function () { var seed = row.querySelector('[data-request-sort-seed-input]'); if (seed) { seed.focus(); } }); } }, sortDirection: function (button) { var row = button.closest('[data-request-sort-rule]'); if (!row || row.classList.contains('is-random')) { return; } row.querySelectorAll('[data-request-sort-direction]').forEach(function (item) { var active = item === button; item.classList.toggle('is-active', active); item.setAttribute('aria-pressed', active ? 'true' : 'false'); }); this.syncSortRules(); }, sortRemove: function (button) { var row = button.closest('[data-request-sort-rule]'); if (row) { row.remove(); this.syncSortRules(); } }, sortStable: function () { if (!this.sortList) { return; } var hasId = Array.from(this.sortList.querySelectorAll('[data-request-sort-source]')).some(function (select) { return select.value === 'document:Id'; }); if (hasId) { Adminx.Toast.show('Финальный порядок по ID уже добавлен', 'info'); return; } this.sortAdd('document:Id'); }, syncSortRules: function () { if (!this.sortList) { return; } var rules = []; this.sortList.querySelectorAll('[data-request-sort-rule]').forEach(function (row, index) { var position = row.querySelector('[data-request-sort-position]'); if (position) { position.textContent = String(index + 1); } var select = row.querySelector('[data-request-sort-source]'); if (!select) { return; } var separator = select.value.indexOf(':'); var source = separator >= 0 ? select.value.slice(0, separator) : ''; var key = separator >= 0 ? select.value.slice(separator + 1) : ''; var activeDirection = row.querySelector('[data-request-sort-direction].is-active'); var rule = {source: source, key: source === 'field' ? parseInt(key, 10) : key, direction: source === 'random' ? 'ASC' : (activeDirection ? activeDirection.getAttribute('data-request-sort-direction') : 'ASC')}; if (source === 'random') { var seedInput = row.querySelector('[data-request-sort-seed-input]'); var seed = seedInput ? seedInput.value.trim() : ''; if (seed !== '') { rule.seed = seed; } } rules.push(rule); }); var input = document.querySelector('[data-request-sort-rules]'); if (input) { input.value = JSON.stringify(rules); } var empty = document.querySelector('[data-request-sort-empty]'); if (empty) { empty.hidden = rules.length > 0; } var stable = document.querySelector('[data-request-sort-stable]'); if (stable) { stable.disabled = rules.some(function (rule) { return rule.source === 'random' || (rule.source === 'document' && rule.key === 'Id'); }); } }, sortDragStart: function (event) { var handle = event.target.closest('[data-request-sort-handle]'); if (!handle) { event.preventDefault(); return; } this.sortDragRow = handle.closest('[data-request-sort-rule]'); if (!this.sortDragRow) { return; } this.sortDragRow.classList.add('is-dragging'); event.dataTransfer.effectAllowed = 'move'; event.dataTransfer.setData('text/plain', 'request-sort'); }, sortDragOver: function (event) { if (!this.sortDragRow || !this.sortList) { return; } event.preventDefault(); var target = event.target.closest('[data-request-sort-rule]'); if (!target || target === this.sortDragRow) { return; } var rect = target.getBoundingClientRect(); this.sortList.insertBefore(this.sortDragRow, event.clientY < rect.top + rect.height / 2 ? target : target.nextSibling); }, sortDrop: function (event) { if (!this.sortDragRow) { return; } event.preventDefault(); this.sortDragEnd(); this.syncSortRules(); }, sortDragEnd: function () { if (this.sortDragRow) { this.sortDragRow.classList.remove('is-dragging'); } this.sortDragRow = null; }, syncEditorAdvancedOptions: function () { var basic = this.editorMode !== 'advanced'; document.querySelectorAll('[data-request-advanced-option]').forEach(function (option) { option.hidden = basic && !option.selected; }); }, onClick: function (e) { var self = this; var editorMode = e.target.closest('[data-request-editor-mode-value]'); if (editorMode) { return this.setEditorMode(editorMode.getAttribute('data-request-editor-mode-value')); } var presentation = e.target.closest('[data-request-presentation-value]'); if (presentation) { return this.selectPresentation(presentation.getAttribute('data-request-presentation-value')); } if (e.target.closest('[data-request-open-code]')) { return this.openCodeEditor(); } if (e.target.closest('[data-request-sort-add]')) { return this.sortAdd(); } if (e.target.closest('[data-request-sort-stable]')) { return this.sortStable(); } var sortDirection = e.target.closest('[data-request-sort-direction]'); if (sortDirection) { return this.sortDirection(sortDirection); } var sortRemove = e.target.closest('[data-request-sort-remove]'); if (sortRemove) { return this.sortRemove(sortRemove); } // список var copy = e.target.closest('[data-request-copy]'); if (copy) { return this.copy(copy.closest('tr')); } var del = e.target.closest('[data-request-delete]'); if (del) { return this.remove(del.closest('tr')); } // массовая сверка Legacy и Native var auditOne = e.target.closest('[data-native-audit-one]'); if (auditOne) { return this.auditOne(auditOne.closest('[data-native-audit-row]'), auditOne); } if (e.target.closest('[data-native-audit-all]')) { return this.auditAll(e.target.closest('[data-native-audit-all]')); } if (e.target.closest('[data-native-audit-stabilize]')) { return this.auditStabilize(); } if (e.target.closest('[data-native-audit-activate]')) { return this.auditActivate(); } if (e.target.closest('[data-native-audit-rollback]')) { return this.auditRollback(); } if (e.target.closest('[data-requests-reset]')) { var input = document.querySelector('[data-requests-search]'); if (input) { input.value = ''; } return this.filterList(''); } // редактор: тулбар var tags = e.target.closest('[data-req-tags-toggle]'); if (tags) { return this.openPalette(tags.closest('[data-req-code-field]')); } if (e.target.closest('[data-req-tags-close]')) { return this.closePalette(); } var lint = e.target.closest('[data-req-lint]'); if (lint) { return this.lint(lint.closest('[data-req-code-field]')); } var full = e.target.closest('[data-req-fullscreen]'); if (full) { return this.fullscreen(full.closest('[data-req-code-field]')); } var preview = e.target.closest('[data-request-preview]'); if (preview) { return this.preview(preview); } var explainOption = e.target.closest('[data-request-explain-option]'); if (explainOption) { return this.selectExplainDocument(explainOption); } var explainRun = e.target.closest('[data-request-explain-run]'); if (explainRun) { return this.explainSelected(explainRun); } var previewExplain = e.target.closest('[data-preview-explain]'); if (previewExplain) { return this.explain(parseInt(previewExplain.getAttribute('data-preview-explain') || '0', 10)); } // палитра var tab = e.target.closest('[data-req-tag-tab]'); if (tab) { return this.activateGroup(tab.getAttribute('data-req-tag-tab')); } var tag = e.target.closest('[data-req-tag]'); if (tag) { return this.insertTag(tag); } // условия var condAdd = e.target.closest('[data-cond-add]'); if (condAdd) { return this.condAdd(condAdd.closest('[data-cond-group]')); } var booleanChoice = e.target.closest('[data-condition-boolean]'); if (booleanChoice) { return this.conditionBooleanValue(booleanChoice); } var documentPick = e.target.closest('[data-condition-document-pick]'); if (documentPick) { return this.conditionDocumentPicker(documentPick.closest('tr')); } var save = e.target.closest('[data-cond-save]'); if (save) { return this.condSave(save.closest('tr')); } var cdel = e.target.closest('[data-cond-delete]'); if (cdel) { return this.condDelete(cdel.closest('tr')); } var groupOperator = e.target.closest('[data-group-operator]'); if (groupOperator) { return this.groupOperator(groupOperator); } var groupAdd = e.target.closest('[data-group-add]'); if (groupAdd) { return this.groupAdd(groupAdd.closest('[data-cond-group]')); } var groupDelete = e.target.closest('[data-group-delete]'); if (groupDelete) { return this.groupDelete(groupDelete.closest('[data-cond-group]')); } }, // ------------------------------------------------------------------ // // Список // ------------------------------------------------------------------ // auditBase: function () { return this.auditPage ? this.auditPage.getAttribute('data-base') : Adminx.base(); }, auditRequest: function (url) { return Adminx.Ajax.post(url, new FormData()).then(function (payload) { var data = payload.data || {}; if (!payload.ok || !data.success) { throw new Error(data.message || 'Не удалось выполнить проверку'); } return data; }); }, auditSetBusy: function (busy) { this.auditRunning = busy; if (!this.auditPage) { return; } this.auditPage.querySelectorAll('[data-native-audit-one]').forEach(function (button) { button.disabled = busy; }); document.querySelectorAll('[data-native-audit-all], [data-native-audit-stabilize], [data-native-audit-activate], [data-native-audit-rollback]').forEach(function (button) { var unavailable = button.hasAttribute('data-native-audit-activate') ? button.getAttribute('data-audit-enabled') !== '1' : (button.hasAttribute('data-native-audit-stabilize') && button.getAttribute('data-audit-enabled') !== '1'); button.disabled = busy || unavailable; }); }, auditProgress: function (done, total, title) { var progress = this.auditPage && this.auditPage.querySelector('[data-native-audit-progress]'); if (!progress) { return; } progress.hidden = false; var heading = progress.querySelector('[data-native-audit-progress-title]'); var copy = progress.querySelector('[data-native-audit-progress-copy]'); var bar = progress.querySelector('[data-native-audit-progress-bar]'); if (heading) { heading.textContent = title || 'Проверка запросов'; } if (copy) { copy.textContent = done + ' из ' + total; } if (bar) { bar.style.width = (total ? Math.round(done / total * 100) : 0) + '%'; } }, auditState: function (row, comparison) { if (!row) { return; } var result = row.querySelector('[data-audit-result]'); var status = comparison && comparison.status ? comparison.status : 'error'; var labels = { matched: ['is-success', 'ti-circle-check', 'Совпадает'], unstable: ['is-warning', 'ti-arrows-sort', 'Порядок не закреплён'], unsupported: ['is-warning', 'ti-code', 'Остаётся Legacy'], order_diff: ['is-warning', 'ti-arrows-sort', 'Отличается порядок'], mismatch: ['is-danger', 'ti-alert-triangle', 'Результаты различаются'], incomplete: ['is-warning', 'ti-hourglass', 'Нужна ручная проверка'], error: ['is-danger', 'ti-alert-triangle', 'Ошибка проверки'] }; var label = labels[status] || labels.error; var detail = ''; if (comparison) { if (comparison.reasons && comparison.reasons.length) { detail = comparison.reasons.join(' '); } else if (comparison.legacy && comparison.native) { detail = 'Legacy: ' + comparison.legacy.total + ' · Native: ' + comparison.native.total; if (comparison.runtime_verification && comparison.runtime_verification.length > 1) { detail += ' · проверено состояний: ' + comparison.runtime_verification.length; } if (comparison.missing_ids && comparison.missing_ids.length) { detail += ' · нет ID: ' + comparison.missing_ids.slice(0, 8).join(', '); } if (comparison.extra_ids && comparison.extra_ids.length) { detail += ' · лишние ID: ' + comparison.extra_ids.slice(0, 8).join(', '); } } var orderDetail = this.auditOrderDetail(comparison.order_diagnostics || {}); if (orderDetail) { detail += (detail ? ' ' : '') + orderDetail; } } result.replaceChildren(); var strong = document.createElement('b'); strong.className = label[0]; var icon = document.createElement('i'); icon.className = 'ti ' + label[1]; strong.appendChild(icon); strong.appendChild(document.createTextNode(label[2])); result.appendChild(strong); if (detail) { var small = document.createElement('small'); small.textContent = detail; small.setAttribute('data-tooltip', detail); result.appendChild(small); } row.setAttribute('data-audit-verified', status === 'matched' ? '1' : '0'); row.setAttribute('data-audit-status', status); }, auditOrderDetail: function (diagnostics) { if (!diagnostics || !diagnostics.position) { return ''; } var detail = 'Позиция ' + diagnostics.position + ': Legacy #' + diagnostics.legacy_id + ', Native #' + diagnostics.native_id + '.'; var criterion = diagnostics.criteria && diagnostics.criteria.length ? diagnostics.criteria[0] : null; if (criterion) { detail += ' ' + criterion.label + ': ' + criterion.legacy_value + ' / ' + criterion.native_value + '.'; } return detail; }, auditOne: function (row, button, quiet) { if (!row || this.auditRunning && !quiet) { return Promise.resolve(false); } var self = this; var id = row.getAttribute('data-native-audit-row'); var icon = button && button.querySelector('i'); if (button) { button.disabled = true; } if (icon) { icon.classList.add('is-spinning'); } return this.auditRequest(this.auditBase() + '/requests/' + encodeURIComponent(id) + '/native-audit').then(function (data) { self.auditState(row, data.data && data.data.comparison ? data.data.comparison : {}); self.auditRefreshStats(); if (!quiet) { Adminx.Toast.show(data.message || 'Запрос проверен', 'success'); } return true; }).catch(function (error) { self.auditState(row, { status: 'error', reasons: [error.message] }); if (!quiet) { Adminx.Toast.show(error.message, 'error'); } return false; }).then(function (result) { if (button) { button.disabled = false; } if (icon) { icon.classList.remove('is-spinning'); } return result; }); }, auditAll: function () { if (!this.auditPage || this.auditRunning) { return; } var self = this; var rows = Array.prototype.slice.call(this.auditPage.querySelectorAll('[data-native-audit-row]')); this.auditSetBusy(true); this.auditProgress(0, rows.length, 'Подготовка Shadow'); this.auditRequest(this.auditBase() + '/requests/native-audit/prepare').then(function () { rows.forEach(function (row) { var mode = row.querySelector('[data-audit-mode]'); if (mode) { mode.className = 'badge badge-blue'; mode.textContent = 'Shadow'; } }); var done = 0; var matched = 0; var run = function () { if (done >= rows.length) { self.auditSetBusy(false); self.auditRefreshStats(); self.auditProgress(done, rows.length, 'Проверка завершена'); Adminx.Toast.show('Проверка завершена: совпало ' + matched + ' из ' + rows.length, matched ? 'success' : 'warning'); return; } self.auditProgress(done, rows.length, 'Проверяется запрос #' + rows[done].getAttribute('data-native-audit-row')); var button = rows[done].querySelector('[data-native-audit-one]'); self.auditOne(rows[done], button, true).then(function (ok) { if (ok && rows[done].getAttribute('data-audit-verified') === '1') { matched++; } done++; self.auditProgress(done, rows.length, 'Проверка запросов'); window.setTimeout(run, 30); }); }; run(); }).catch(function (error) { self.auditSetBusy(false); self.auditProgress(0, rows.length, 'Проверка остановлена'); Adminx.Toast.show(error.message, 'error'); }); }, auditRefreshStats: function () { if (!this.auditPage) { return; } var rows = this.auditPage.querySelectorAll('[data-native-audit-row]'); var verified = this.auditPage.querySelectorAll('[data-audit-verified="1"]').length; var nativeCount = 0; rows.forEach(function (row) { var mode = row.querySelector('[data-audit-mode]'); if (mode && mode.textContent.trim().toLowerCase() === 'native') { nativeCount++; } }); var verifiedStat = document.querySelector('[data-audit-stat="verified"]'); var nativeStat = document.querySelector('[data-audit-stat="native"]'); if (verifiedStat) { verifiedStat.textContent = String(verified); } if (nativeStat) { nativeStat.textContent = String(nativeCount); } var activate = document.querySelector('[data-native-audit-activate]'); if (activate) { activate.setAttribute('data-audit-enabled', verified ? '1' : '0'); activate.disabled = this.auditRunning || !verified; } var unstable = this.auditPage.querySelectorAll('[data-audit-status="unstable"], [data-audit-status="order_diff"]').length; var stabilize = document.querySelector('[data-native-audit-stabilize]'); if (stabilize) { stabilize.setAttribute('data-audit-enabled', unstable ? '1' : '0'); stabilize.disabled = this.auditRunning || !unstable; } }, auditStabilize: function () { if (!this.auditPage || this.auditRunning) { return; } var self = this; var rows = Array.prototype.slice.call(this.auditPage.querySelectorAll('[data-audit-status="unstable"], [data-audit-status="order_diff"]')); if (!rows.length) { Adminx.Toast.show('Запросов с незакреплённым порядком нет', 'info'); return; } Adminx.Confirm.open({ kind: 'warning', title: 'Закрепить порядок без изменения выдачи?', message: 'Для каждого запроса полная Legacy-последовательность будет сверена с ID по возрастанию и убыванию. Native включится только при точном совпадении; любое расхождение автоматически отменит изменение.', confirmLabel: 'Проверить и закрепить', onConfirm: function () { self.auditSetBusy(true); var done = 0; var applied = 0; var skipped = 0; var run = function () { if (done >= rows.length) { self.auditSetBusy(false); self.auditRefreshStats(); self.auditProgress(done, rows.length, 'Порядок проверен'); var message = 'Порядок закреплён: ' + applied; if (skipped) { message += ' · без изменений: ' + skipped; } Adminx.Toast.show(message, applied ? 'success' : 'warning'); window.setTimeout(function () { window.location.reload(); }, 650); return; } var row = rows[done]; var id = row.getAttribute('data-native-audit-row'); var originalStatus = row.getAttribute('data-audit-status') || 'unstable'; self.auditProgress(done, rows.length, 'Проверяется порядок запроса #' + id); self.auditRequest(self.auditBase() + '/requests/' + encodeURIComponent(id) + '/native-stabilize').then(function (data) { var result = data.data || {}; if (result.status === 'applied' && result.comparison) { applied++; self.auditState(row, result.comparison); var mode = row.querySelector('[data-audit-mode]'); if (mode) { mode.className = 'badge badge-green'; mode.textContent = 'Native'; } var plan = row.querySelector('[data-audit-plan]'); if (plan) { plan.className = 'badge badge-blue'; plan.textContent = 'Готов'; } } else { skipped++; var reason = result.message || 'Безопасный порядок не найден'; self.auditState(row, { status: originalStatus, reasons: [reason] }); } }).catch(function (error) { skipped++; self.auditState(row, { status: 'error', reasons: [error.message] }); }).then(function () { done++; self.auditRefreshStats(); self.auditProgress(done, rows.length, 'Закрепление порядка'); window.setTimeout(run, 30); }); }; self.auditProgress(0, rows.length, 'Подготовка безопасной проверки'); run(); } }); }, auditActivate: function () { if (this.auditRunning) { return; } var self = this; Adminx.Confirm.open({ kind: 'warning', title: 'Включить Native executor?', message: 'Новый путь включится только у запросов с точным совпадением текущего плана. Остальные продолжат работать через Legacy.', confirmLabel: 'Включить подтверждённые', onConfirm: function () { self.auditSetBusy(true); self.auditRequest(self.auditBase() + '/requests/native-audit/activate').then(function (data) { Adminx.Toast.show((data.message || 'Native включён') + ': ' + ((data.data && data.data.count) || 0), 'success'); window.location.reload(); }).catch(function (error) { self.auditSetBusy(false); Adminx.Toast.show(error.message, 'error'); }); } }); }, auditRollback: function () { if (this.auditRunning) { return; } var self = this; Adminx.Confirm.open({ kind: 'warning', title: 'Вернуть все запросы в Legacy?', message: 'Публичный сайт сразу вернётся к прежнему executor. Результаты проверки сохранятся.', confirmLabel: 'Вернуть Legacy', onConfirm: function () { self.auditSetBusy(true); self.auditRequest(self.auditBase() + '/requests/native-audit/rollback').then(function (data) { Adminx.Toast.show(data.message || 'Legacy восстановлен', 'success'); window.location.reload(); }).catch(function (error) { self.auditSetBusy(false); Adminx.Toast.show(error.message, 'error'); }); } }); }, filterList: function (query) { var root = document.querySelector('[data-requests-page]'); if (!root) { return; } var q = String(query || '').trim().toLowerCase(); var rows = root.querySelectorAll('[data-requests-list] tr[data-request-row]'); var visible = 0; rows.forEach(function (row) { var show = !q || (row.getAttribute('data-search') || '').indexOf(q) !== -1; row.hidden = !show; if (show) { visible++; } }); var count = root.querySelector('[data-requests-count]'); if (count) { count.textContent = String(visible); } var summary = root.querySelector('[data-requests-summary]'); if (summary) { summary.textContent = q ? (visible + ' из ' + rows.length + ' по фильтру.') : (rows.length + ' запросов в списке.'); } var empty = root.querySelector('[data-requests-empty]'); if (empty) { empty.hidden = !(q && visible === 0); } var reset = root.querySelector('[data-requests-reset]'); if (reset) { reset.hidden = !q; } }, copy: function (row) { var base = Adminx.base(); Adminx.Loader.show(); Adminx.Ajax.post(base + '/requests/' + row.dataset.id + '/copy').then(function (p) { Adminx.Loader.hide(); var d = p.data || {}; if (d.success && d.redirect) { window.location.href = d.redirect; } else { Adminx.Toast.show(d.message || 'Не удалось', 'error'); } }).catch(function () { Adminx.Loader.hide(); Adminx.Toast.show('Ошибка сети', 'error'); }); }, remove: function (row) { var base = Adminx.base(); Adminx.Confirm.open({ kind: 'error', title: 'Удалить запрос?', message: 'Запрос «' + row.dataset.alias + '» и его условия будут удалены.', confirmLabel: 'Удалить', confirmClass: 'btn-danger', onConfirm: function () { Adminx.Loader.show(); Adminx.Ajax.post(base + '/requests/' + row.dataset.id + '/delete').then(function (p) { Adminx.Loader.hide(); var d = p.data || {}; if (d.success) { row.remove(); Adminx.Toast.show(d.message, 'success'); } else { Adminx.Toast.show(d.message || 'Ошибка', 'error'); } }).catch(function () { Adminx.Loader.hide(); Adminx.Toast.show('Ошибка сети', 'error'); }); } }); }, // ------------------------------------------------------------------ // // Сохранение запроса // ------------------------------------------------------------------ // save: function () { var self = this; this.form.querySelectorAll('[data-error]').forEach(function (s) { s.textContent = ''; }); var id = this.form.getAttribute('data-id') || '0'; var url = this.base() + '/requests' + (id !== '0' ? '/' + id : ''); Adminx.Loader.show(); Adminx.Ajax.post(url, new FormData(this.form)).then(function (p) { Adminx.Loader.hide(); var d = p.data || {}; if (d.success) { if (d.redirect) { window.location.href = d.redirect; return; } Adminx.Toast.show(d.message || 'Сохранено', 'success'); return; } var errors = d.errors || {}; Object.keys(errors).forEach(function (f) { var span = self.form.querySelector('[data-error="' + f + '"]'); if (span) { span.textContent = errors[f]; } var inp = self.form.querySelector('[name="' + f + '"]'); if (inp) { inp.classList.add('is-invalid'); } }); Adminx.Toast.show(d.message || 'Проверьте поля', 'error'); }).catch(function () { Adminx.Loader.hide(); Adminx.Toast.show('Ошибка сети', 'error'); }); }, preview: function (button) { if (!this.form || !button || button.disabled) { return; } var id = parseInt(this.form.getAttribute('data-id') || '0', 10); if (id <= 0) { return; } var fd = new FormData(); this.form.querySelectorAll('[name="result_system[]"]:checked').forEach(function (input) { fd.append('result_system[]', input.value); }); this.form.querySelectorAll('[name="result_fields[]"]:checked').forEach(function (input) { fd.append('result_fields[]', input.value); }); var limit = document.querySelector('[data-request-preview-limit]'); fd.append('limit', limit ? limit.value : '10'); var renderer = document.querySelector('[data-request-preview-renderer]'); fd.append('renderer', renderer ? renderer.value : 'data_cards'); var self = this; button.disabled = true; button.classList.add('is-loading'); Adminx.Ajax.post(this.base() + '/requests/' + id + '/preview', fd).then(function (payload) { var response = payload.data || {}; if (!response.success) { Adminx.Toast.show(response.message || 'Предпросмотр не выполнен', 'error'); return; } self.renderPreview(response.data || {}); Adminx.Toast.show(response.message || 'Предпросмотр обновлён', 'success'); }).catch(function () { Adminx.Toast.show('Ошибка сети при построении предпросмотра', 'error'); }).then(function () { button.disabled = false; button.classList.remove('is-loading'); }); }, renderPreview: function (data) { var section = document.querySelector('[data-request-preview-section]'); if (!section) { return; } var empty = section.querySelector('[data-request-preview-empty]'); var result = section.querySelector('[data-request-preview-result]'); if (empty) { empty.hidden = true; } if (result) { result.hidden = false; } this.setText(section, '[data-preview-total]', String(data.total || 0)); this.setText(section, '[data-preview-returned]', String((data.items || []).length)); this.setText(section, '[data-preview-time]', this.previewMilliseconds(data.query_time)); this.renderPreviewPlan(section.querySelector('[data-request-preview-plan]'), data.plan || {}); this.renderExecutorComparison(section.querySelector('[data-request-executor-comparison]'), data.executor_comparison || {}); this.renderPreviewItems(section.querySelector('[data-request-preview-items]'), data); var sql = section.querySelector('[data-request-preview-sql]'); if (sql) { sql.textContent = data.sql || 'SQL доступен только пользователю с правом управления запросами.'; } }, renderExecutorComparison: function (root, comparison) { if (!root) { return; } root.textContent = ''; var status = comparison.status || ''; if (!status) { root.hidden = true; return; } root.hidden = false; var states = { matched: ['is-success', 'ti-circle-check', 'Legacy и Native совпали', 'Количество, состав и порядок документов одинаковы.'], unstable: ['is-warning', 'ti-arrows-sort', 'Состав совпал, порядок не закреплён', 'Публичный вывод остаётся на Legacy, пока одинаковые значения сортировки не получат безопасный финальный порядок.'], order_diff: ['is-warning', 'ti-arrows-sort', 'Состав совпал, порядок отличается', 'До переключения проверьте сортировку запроса.'], mismatch: ['is-danger', 'ti-alert-triangle', 'Результаты различаются', 'Native включать рано: сравните отсутствующие и лишние ID.'], incomplete: ['is-warning', 'ti-database-exclamation', 'Выборка слишком велика для автоподтверждения', 'Проверены первые 5000 документов. Оставьте Legacy или разбейте представление на более узкие запросы.'], unsupported: ['is-warning', 'ti-code', 'Запрос пока требует Legacy', 'В плане есть PHP, свободный SQL или неподдерживаемая операция.'], error: ['is-danger', 'ti-alert-circle', 'Сравнение не выполнено', 'Legacy продолжает работать; ошибка записана в системный журнал.'] }; var state = states[status] || states.error; var nativeOption = document.querySelector('[data-native-executor-option]'); var nativeState = document.querySelector('[data-native-plan-state]'); if (nativeOption && !nativeOption.selected) { nativeOption.disabled = status !== 'matched'; } if (nativeState) { nativeState.className = 'badge ' + (status === 'matched' ? 'badge-green' : (status === 'unsupported' ? 'badge-amber' : 'badge-blue')); nativeState.textContent = status === 'matched' ? 'Native подтверждён' : (status === 'unsupported' ? 'Требуется Legacy' : (status === 'unstable' ? 'Нужен стабильный порядок' : 'Нужно проверить')); } root.className = 'request-executor-comparison ' + state[0]; var icon = document.createElement('span'); icon.className = 'icon-tile'; icon.innerHTML = ''; var copy = document.createElement('div'); var title = document.createElement('b'); title.textContent = state[2]; var description = document.createElement('span'); description.textContent = state[3]; copy.appendChild(title); copy.appendChild(description); root.appendChild(icon); root.appendChild(copy); var details = []; if (comparison.legacy && comparison.native) { details.push('Legacy: ' + comparison.legacy.total); details.push('Native: ' + comparison.native.total); } if (comparison.runtime_verification && comparison.runtime_verification.length > 1) { details.push('Runtime-состояний: ' + comparison.runtime_verification.length); } if ((comparison.missing_ids || []).length) { details.push('Нет в Native: #' + comparison.missing_ids.join(', #')); } if ((comparison.extra_ids || []).length) { details.push('Лишние в Native: #' + comparison.extra_ids.join(', #')); } (comparison.reasons || []).forEach(function (reason) { details.push(reason); }); if (details.length) { var meta = document.createElement('small'); meta.textContent = details.join(' · '); root.appendChild(meta); } }, setText: function (root, selector, value) { var node = root && root.querySelector(selector); if (node) { node.textContent = value; } }, previewMilliseconds: function (seconds) { var value = Number(seconds || 0) * 1000; if (!isFinite(value)) { value = 0; } return (value < 10 ? value.toFixed(2) : value.toFixed(0)) + ' мс'; }, renderPreviewPlan: function (root, plan) { if (!root) { return; } root.textContent = ''; var items = [ ['ti-category', 'Рубрика #' + (plan.rubric_id || 0)], ['ti-brackets', (plan.groups || 0) + ' групп'], ['ti-filter-check', (plan.conditions || 0) + ' активных условий'], ['ti-sort-descending', plan.order || 'Без явной сортировки'], ['ti-calendar-check', plan.publication || 'Проверка публикации'], ]; items.forEach(function (item) { var chip = document.createElement('span'); var icon = document.createElement('i'); icon.className = 'ti ' + item[0]; chip.appendChild(icon); chip.appendChild(document.createTextNode(item[1])); root.appendChild(chip); }); }, renderPreviewItems: function (root, data) { if (!root) { return; } root.textContent = ''; var items = Array.isArray(data.items) ? data.items : []; var renderer = data.renderer || {}; if (renderer.presentation === 'json') { var json = document.createElement('pre'); json.className = 'request-preview-json'; json.textContent = JSON.stringify({ total: Number(data.total || 0), limit: Number(data.limit || 0), items: items }, null, 2); root.appendChild(json); return; } if (!items.length) { var empty = document.createElement('div'); empty.className = 'request-preview-no-items'; empty.innerHTML = 'Документы не найденыПроверьте условия, статус публикации и выбранную рубрику.'; root.appendChild(empty); return; } var systemOptions = data.system_options || {}; var fieldOptions = data.field_options || {}; if (renderer.presentation === 'list') { this.renderPreviewList(root, items, systemOptions, fieldOptions); return; } if (renderer.presentation === 'table') { this.renderPreviewTable(root, items, systemOptions, fieldOptions); return; } items.forEach(function (item, index) { var card = document.createElement('article'); card.className = 'request-preview-item'; card.style.setProperty('--preview-order', String(Math.min(index, 8))); var head = document.createElement('header'); var title = document.createElement('div'); var titleText = item.system && item.system.title ? String(item.system.title) : 'Документ #' + item.document_id; title.innerHTML = ''; var copy = document.createElement('span'); var strong = document.createElement('b'); strong.textContent = titleText; var small = document.createElement('small'); small.textContent = '#' + item.document_id; copy.appendChild(strong); copy.appendChild(small); title.appendChild(copy); head.appendChild(title); var explain = document.createElement('button'); explain.className = 'btn btn-ghost btn-icon btn-sm ax-act ax-act-view'; explain.type = 'button'; explain.setAttribute('data-preview-explain', String(item.document_id)); explain.setAttribute('data-tooltip', 'Объяснить результат'); explain.setAttribute('aria-label', 'Объяснить результат'); explain.innerHTML = ''; head.appendChild(explain); card.appendChild(head); var values = document.createElement('dl'); values.className = 'request-preview-values'; Object.keys(item.system || {}).forEach(function (key) { if (key === 'title') { return; } Adminx.Requests.appendPreviewValue(values, systemOptions[key] || key, item.system[key], key); }); Object.keys(item.fields || {}).forEach(function (key) { var field = item.fields[key] || {}; var option = fieldOptions[key] || {}; var label = option.title || field.alias || ('Поле #' + key); Adminx.Requests.appendPreviewValue(values, label, field.value, field.alias || key); }); card.appendChild(values); root.appendChild(card); }); }, renderPreviewList: function (root, items, systemOptions, fieldOptions) { items.forEach(function (item, index) { var row = document.createElement('article'); row.className = 'request-preview-list-item'; row.style.setProperty('--preview-order', String(Math.min(index, 8))); var main = document.createElement('div'); main.className = 'request-preview-list-main'; main.innerHTML = ''; var copy = document.createElement('span'); var title = document.createElement('b'); title.textContent = Adminx.Requests.previewTitle(item); var id = document.createElement('small'); id.textContent = '#' + item.document_id; copy.appendChild(title); copy.appendChild(id); main.appendChild(copy); row.appendChild(main); var values = document.createElement('div'); values.className = 'request-preview-list-values'; Adminx.Requests.previewEntries(item, systemOptions, fieldOptions).slice(0, 3).forEach(function (entry) { var value = document.createElement('span'); var label = document.createElement('small'); label.textContent = entry.label; var text = document.createElement('b'); text.textContent = Adminx.Requests.previewValue(entry.value); value.appendChild(label); value.appendChild(text); values.appendChild(value); }); row.appendChild(values); row.appendChild(Adminx.Requests.previewExplainButton(item.document_id)); root.appendChild(row); }); }, renderPreviewTable: function (root, items, systemOptions, fieldOptions) { var columns = []; var known = {}; items.forEach(function (item) { Adminx.Requests.previewEntries(item, systemOptions, fieldOptions).forEach(function (entry) { if (!known[entry.key]) { known[entry.key] = true; columns.push({key: entry.key, label: entry.label}); } }); }); var wrap = document.createElement('div'); wrap.className = 'request-preview-table-wrap'; var table = document.createElement('table'); table.className = 'table request-preview-table'; var head = document.createElement('thead'); var headRow = document.createElement('tr'); var titleHead = document.createElement('th'); titleHead.textContent = 'Материал'; headRow.appendChild(titleHead); columns.forEach(function (column) { var th = document.createElement('th'); th.textContent = column.label; headRow.appendChild(th); }); var actionHead = document.createElement('th'); actionHead.className = 'request-preview-table-action'; headRow.appendChild(actionHead); head.appendChild(headRow); table.appendChild(head); var body = document.createElement('tbody'); items.forEach(function (item) { var values = {}; Adminx.Requests.previewEntries(item, systemOptions, fieldOptions).forEach(function (entry) { values[entry.key] = entry.value; }); var tr = document.createElement('tr'); var titleCell = document.createElement('td'); titleCell.className = 'request-preview-table-title'; var title = document.createElement('b'); title.textContent = Adminx.Requests.previewTitle(item); var id = document.createElement('small'); id.textContent = '#' + item.document_id; titleCell.appendChild(title); titleCell.appendChild(id); tr.appendChild(titleCell); columns.forEach(function (column) { var td = document.createElement('td'); td.textContent = Adminx.Requests.previewValue(Object.prototype.hasOwnProperty.call(values, column.key) ? values[column.key] : null); tr.appendChild(td); }); var action = document.createElement('td'); action.className = 'request-preview-table-action'; action.appendChild(Adminx.Requests.previewExplainButton(item.document_id)); tr.appendChild(action); body.appendChild(tr); }); table.appendChild(body); wrap.appendChild(table); root.appendChild(wrap); }, previewTitle: function (item) { return item.system && item.system.title ? String(item.system.title) : 'Документ #' + item.document_id; }, previewEntries: function (item, systemOptions, fieldOptions) { var entries = []; Object.keys(item.system || {}).forEach(function (key) { if (key !== 'title') { entries.push({key: 'system.' + key, label: systemOptions[key] || key, value: item.system[key]}); } }); Object.keys(item.fields || {}).forEach(function (key) { var field = item.fields[key] || {}; var option = fieldOptions[key] || {}; entries.push({key: 'field.' + key, label: option.title || field.alias || ('Поле #' + key), value: field.value}); }); return entries; }, previewExplainButton: function (documentId) { var button = document.createElement('button'); button.className = 'btn btn-ghost btn-icon btn-sm ax-act ax-act-view'; button.type = 'button'; button.setAttribute('data-preview-explain', String(documentId)); button.setAttribute('data-tooltip', 'Объяснить результат'); button.setAttribute('aria-label', 'Объяснить результат'); button.innerHTML = ''; return button; }, appendPreviewValue: function (root, label, value, code) { var group = document.createElement('div'); var term = document.createElement('dt'); term.textContent = label; var system = document.createElement('small'); system.textContent = code; term.appendChild(system); var description = document.createElement('dd'); description.textContent = this.previewValue(value); group.appendChild(term); group.appendChild(description); root.appendChild(group); }, previewValue: function (value) { if (value === null || typeof value === 'undefined' || value === '') { return '—'; } if (typeof value === 'boolean') { return value ? 'Да' : 'Нет'; } if (typeof value === 'object') { try { return JSON.stringify(value, null, 2); } catch (e) { return '[структурированные данные]'; } } return String(value); }, explainSearchInput: function (input) { input.removeAttribute('data-document-id'); var button = document.querySelector('[data-request-explain-run]'); var directId = /^\s*#?\d+\s*$/.test(input.value) ? parseInt(input.value.replace(/\D/g, ''), 10) : 0; if (directId > 0) { input.setAttribute('data-document-id', String(directId)); if (button) { button.disabled = false; } } else if (button) { button.disabled = true; } window.clearTimeout(this.explainSearchTimer); var self = this; this.explainSearchTimer = window.setTimeout(function () { self.searchExplainDocuments(input); }, 220); }, searchExplainDocuments: function (input) { if (!this.form || !input) { return; } var id = parseInt(this.form.getAttribute('data-id') || '0', 10); if (id <= 0) { return; } var options = document.querySelector('[data-request-explain-options]'); if (!options) { return; } var query = String(input.value || '').trim(); var url = this.base() + '/requests/' + id + '/explain-documents?q=' + encodeURIComponent(query) + '&limit=12'; options.hidden = false; options.innerHTML = '
'; fetch(url, { headers: { 'Accept': 'application/json' }, credentials: 'same-origin' }).then(function (response) { if (!response.ok) { throw new Error('HTTP ' + response.status); } return response.json(); }).then(function (payload) { var response = payload || {}; var items = response.data && response.data.items ? response.data.items : []; Adminx.Requests.renderExplainOptions(options, items); }).catch(function () { options.innerHTML = ''; }); }, renderExplainOptions: function (root, items) { root.textContent = ''; if (!items.length) { root.innerHTML = ''; return; } items.forEach(function (item) { var button = document.createElement('button'); button.type = 'button'; button.setAttribute('data-request-explain-option', String(item.id)); button.setAttribute('data-title', item.title || ('Документ #' + item.id)); var icon = document.createElement('span'); icon.className = 'icon-tile'; icon.innerHTML = ''; var copy = document.createElement('span'); var title = document.createElement('b'); title.textContent = item.title || ('Документ #' + item.id); var meta = document.createElement('small'); meta.textContent = '#' + item.id + (item.alias ? ' · ' + item.alias : '') + (item.deleted ? ' · удалён' : (!item.status ? ' · снят с публикации' : '')); copy.appendChild(title); copy.appendChild(meta); button.appendChild(icon); button.appendChild(copy); root.appendChild(button); }); }, selectExplainDocument: function (option) { var input = document.querySelector('[data-request-explain-search]'); var root = document.querySelector('[data-request-explain-options]'); var id = parseInt(option.getAttribute('data-request-explain-option') || '0', 10); if (!input || id <= 0) { return; } input.value = '#' + id + ' · ' + (option.getAttribute('data-title') || 'Документ'); input.setAttribute('data-document-id', String(id)); if (root) { root.hidden = true; } var run = document.querySelector('[data-request-explain-run]'); if (run) { run.disabled = false; run.focus(); } }, explainSelected: function (button) { var input = document.querySelector('[data-request-explain-search]'); var id = input ? parseInt(input.getAttribute('data-document-id') || '0', 10) : 0; if (id > 0) { this.explain(id, button); } }, explain: function (documentId, button) { if (!this.form || documentId <= 0) { return; } var requestId = parseInt(this.form.getAttribute('data-id') || '0', 10); if (requestId <= 0) { return; } var drawer = document.getElementById('requestExplainDrawer'); if (!drawer) { return; } this.resetExplainDrawer(drawer); if (Adminx.Drawer) { Adminx.Drawer.open('requestExplainDrawer'); } if (button) { button.disabled = true; } var fd = new FormData(); fd.append('document_id', String(documentId)); Adminx.Ajax.post(this.base() + '/requests/' + requestId + '/explain', fd).then(function (payload) { var response = payload.data || {}; if (!response.success) { Adminx.Requests.renderExplainError(drawer, response.message || 'Проверка не выполнена'); return; } Adminx.Requests.renderExplain(drawer, response.data || {}); }).catch(function () { Adminx.Requests.renderExplainError(drawer, 'Ошибка сети при проверке документа'); }).then(function () { if (button) { button.disabled = false; } }); }, resetExplainDrawer: function (drawer) { var loading = drawer.querySelector('[data-request-explain-loading]'); var content = drawer.querySelector('[data-request-explain-content]'); if (loading) { loading.hidden = false; loading.innerHTML = 'Проверяем запрос…'; } if (content) { content.hidden = true; } this.setText(drawer, '[data-request-explain-title]', 'Проверка документа'); this.setText(drawer, '[data-request-explain-subtitle]', 'Условия запроса и фактический итог executor.'); this.setText(drawer, '[data-request-explain-footer]', 'Выполняется'); var footer = drawer.querySelector('[data-request-explain-footer]'); if (footer) { footer.className = 'badge badge-gray'; } var icon = drawer.querySelector('[data-request-explain-icon]'); if (icon) { icon.className = 'dialog-icon info'; icon.innerHTML = ''; } }, renderExplainError: function (drawer, message) { var loading = drawer.querySelector('[data-request-explain-loading]'); if (loading) { loading.hidden = false; loading.innerHTML = ''; loading.querySelector('span').textContent = message; } this.setText(drawer, '[data-request-explain-footer]', 'Ошибка'); var footer = drawer.querySelector('[data-request-explain-footer]'); if (footer) { footer.className = 'badge badge-red'; } var icon = drawer.querySelector('[data-request-explain-icon]'); if (icon) { icon.className = 'dialog-icon danger'; icon.innerHTML = ''; } }, renderExplain: function (drawer, data) { var loading = drawer.querySelector('[data-request-explain-loading]'); var content = drawer.querySelector('[data-request-explain-content]'); if (loading) { loading.hidden = true; } if (content) { content.hidden = false; } var documentData = data.document || {}; var matched = !!data.matched; this.setText(drawer, '[data-request-explain-title]', documentData.title || ('Документ #' + documentData.id)); this.setText(drawer, '[data-request-explain-subtitle]', '#' + documentData.id + (documentData.alias ? ' · ' + documentData.alias : '')); this.setText(drawer, '[data-request-explain-footer]', matched ? 'Входит в запрос' : 'Исключён'); var footer = drawer.querySelector('[data-request-explain-footer]'); if (footer) { footer.className = 'badge ' + (matched ? 'badge-green' : 'badge-red'); } var icon = drawer.querySelector('[data-request-explain-icon]'); if (icon) { icon.className = 'dialog-icon ' + (matched ? 'success' : 'danger'); icon.innerHTML = ''; } this.renderExplainChecks(drawer.querySelector('[data-request-explain-base]'), data.base || []); this.renderExplainTree(drawer.querySelector('[data-request-explain-tree]'), data.groups || []); this.renderExplainNotes(drawer.querySelector('[data-request-explain-notes]'), data.notes || []); }, renderExplainChecks: function (root, checks) { if (!root) { return; } root.textContent = ''; checks.forEach(function (check) { var item = document.createElement('article'); item.className = 'request-explain-check is-' + check.state; item.innerHTML = 'Поиск по ID, названию или адресу.