Files
2026-07-30 11:56:32 +03:00

457 lines
22 KiB
JavaScript

/** Реестр модулей: клиентские фильтры и Ajax lifecycle-действия. */
(function (window, document) {
'use strict';
var Adminx = window.Adminx || (window.Adminx = {});
Adminx.Modules = {
init: function () {
if (!document.querySelector('[data-modules-page]')) { return; }
var self = this;
var page = document.querySelector('[data-modules-page]');
var search = document.querySelector('[data-module-search]');
var state = document.querySelector('[data-module-state]');
var sort = document.querySelector('[data-module-sort]');
var repositorySort = document.querySelector('[data-repository-sort]');
this.focusRequestedModule(page, search, state);
if (search) { search.addEventListener('input', function () { self.filter(); }); }
if (state) { state.addEventListener('change', function () { self.filter(); }); }
if (sort) { sort.addEventListener('change', function () { self.sortRows('[data-module-row]', sort.value); }); }
if (repositorySort) { repositorySort.addEventListener('change', function () { self.sortRows('[data-repository-row]', repositorySort.value); }); }
document.addEventListener('change', function (event) {
var toggle = event.target.closest('[data-module-toggle]');
if (toggle) { self.toggle(toggle); return; }
});
document.addEventListener('click', function (event) {
var refresh = event.target.closest('[data-module-repository-refresh]');
if (refresh) { self.refreshRepository(refresh); return; }
var official = event.target.closest('[data-module-repository-official]');
if (official) { self.restoreOfficialRepository(official); return; }
var remoteInstall = event.target.closest('[data-module-repository-install]');
if (remoteInstall) { self.installRepository(remoteInstall); return; }
var repositoryMigration = event.target.closest('[data-module-repository-migrate]');
if (repositoryMigration) { self.updateRepositoryMigration(repositoryMigration); return; }
var presentation = event.target.closest('[data-module-presentation]');
if (presentation) { self.openPresentation(presentation); return; }
var button = event.target.closest('[data-module-action]');
if (button) { self.action(button); }
});
var presentationForm = document.querySelector('[data-module-presentation-form]');
if (presentationForm) {
presentationForm.addEventListener('submit', function (event) { self.savePresentation(event); });
}
var repositoryForm = document.querySelector('[data-module-repository-form]');
if (repositoryForm) {
repositoryForm.addEventListener('submit', function (event) { self.saveRepository(event); });
}
if (new URLSearchParams(window.location.search).get('tab') === 'catalog' && Adminx.Tabs) {
Adminx.Tabs.activate(page, 'catalog');
}
this.initArchive();
},
focusRequestedModule: function (page, search, state) {
var params = new URLSearchParams(window.location.search);
var code = (params.get('focus') || '').trim().toLowerCase();
if (!code) { return; }
if (params.get('tab') !== 'catalog' && search) {
search.value = code;
if (state) { state.value = 'all'; }
this.filter();
}
window.setTimeout(function () {
var selector = params.get('tab') === 'catalog' ? '[data-repository-row]' : '[data-module-row]';
var row = page.querySelector(selector + '[data-code="' + CSS.escape(code) + '"]');
if (!row) { return; }
row.classList.add('is-focused');
row.scrollIntoView({ behavior: 'smooth', block: 'center' });
}, 80);
},
base: function () { return Adminx.base(); },
filter: function () {
var query = (document.querySelector('[data-module-search]').value || '').trim().toLowerCase();
var state = document.querySelector('[data-module-state]').value;
var shown = 0;
document.querySelectorAll('[data-module-row]').forEach(function (row) {
var matchQuery = !query || row.dataset.search.indexOf(query) !== -1;
var matchState = state === 'all' || row.dataset.state === state;
row.hidden = !(matchQuery && matchState);
if (!row.hidden) { shown++; }
});
var empty = document.querySelector('[data-module-empty]');
if (empty) { empty.hidden = shown !== 0; }
},
sortRows: function (selector, direction) {
var rows = Array.prototype.slice.call(document.querySelectorAll(selector));
if (rows.length < 2) { return; }
var body = rows[0].parentNode;
var empty = body.querySelector('[data-module-empty]');
rows.sort(function (left, right) {
var result = (left.dataset.name || '').localeCompare(right.dataset.name || '', 'ru', { numeric: true, sensitivity: 'base' });
return direction === 'desc' ? -result : result;
});
rows.forEach(function (row) { empty ? body.insertBefore(row, empty) : body.appendChild(row); });
},
post: function (url, formData) {
Adminx.Loader.show();
return Adminx.Ajax.post(url, formData || new FormData()).then(function (payload) {
Adminx.Loader.hide();
return payload.data || {};
}).catch(function (error) {
Adminx.Loader.hide();
Adminx.Toast.show(error && error.message ? error.message : 'Ошибка запроса', 'error');
return { success: false };
});
},
toggle: function (input) {
var self = this;
var row = input.closest('[data-module-row]');
var enabled = input.checked;
input.disabled = true;
var data = new FormData();
data.append('enabled', enabled ? '1' : '0');
this.post(this.base() + '/modules/lifecycle/' + encodeURIComponent(row.dataset.code) + '/toggle', data).then(function (result) {
input.disabled = false;
if (!result.success) {
input.checked = !enabled;
Adminx.Toast.show(result.message || 'Не удалось изменить состояние модуля', 'error');
return;
}
Adminx.Toast.show(result.message || (enabled ? 'Модуль включён' : 'Модуль отключён'), 'success');
self.reloadSoon();
});
},
action: function (button) {
var self = this;
var row = button.closest('[data-module-row]');
var code = row.dataset.code;
var action = button.dataset.moduleAction;
var config = {
install: { title: 'Установить модуль?', message: 'Будут выполнены SQL-команды установки и модуль будет включён.', label: 'Установить', kind: 'info' },
update: { title: 'Обновить модуль?', message: 'Будут выполнены SQL-команды обновления до версии из файлов.', label: 'Обновить', kind: 'warning' },
repair: { title: 'Восстановить операцию модуля?', message: 'Успешные миграции будут пропущены, а оборванный этап выполнится повторно. Перед продолжением проверьте указанную ошибку.', label: 'Восстановить', kind: 'warning' },
reinstall: { title: 'Переустановить модуль?', message: 'Данные и настройки будут удалены, затем модуль установится заново. Это действие необратимо.', label: 'Переустановить', kind: 'error' },
uninstall: { title: 'Деинсталлировать модуль?', message: 'Модуль, его данные, настройки, меню и права будут удалены. Это действие необратимо.', label: 'Деинсталлировать', kind: 'error' },
remove: { title: 'Удалить файлы модуля?', message: 'Каталог пакета будет физически удалён. Для повторной установки понадобится ZIP-архив.', label: 'Удалить файлы', kind: 'error' }
}[action];
if (!config) { return; }
if (action === 'uninstall' || action === 'reinstall') {
button.disabled = true;
this.post(this.base() + '/modules/lifecycle/' + encodeURIComponent(code) + '/preflight/' + action, new FormData()).then(function (result) {
button.disabled = false;
if (!result.success) { return; }
var preflight = result.data || {};
self.confirmAction(button, code, action, config, preflight);
});
return;
}
this.confirmAction(button, code, action, config, null);
},
confirmAction: function (button, code, action, config, preflight) {
var self = this;
var message = this.preflightMessage(config.message, preflight && preflight.preview ? preflight.preview : {});
Adminx.Confirm.open({
kind: config.kind,
title: config.title,
message: message,
confirmLabel: config.label,
confirmClass: config.kind === 'error' ? 'btn-danger' : 'btn-primary',
onConfirm: function () {
button.disabled = true;
var data = new FormData();
if (preflight && preflight.required && preflight.token) {
data.append('preflight_token', preflight.token);
}
self.post(self.base() + '/modules/lifecycle/' + encodeURIComponent(code) + '/' + action, data).then(function (result) {
button.disabled = false;
Adminx.Toast.show(result.message || (result.success ? 'Готово' : 'Операция не выполнена'), result.success ? 'success' : 'error');
if (result.success) { self.reloadSoon(); }
});
}
});
},
preflightMessage: function (fallback, preview) {
var parts = [];
if (preview && preview.message) { parts.push(String(preview.message)); }
var summary = preview && Array.isArray(preview.summary) ? preview.summary : [];
summary.forEach(function (item) {
if (!item || !item.label) { return; }
parts.push(String(item.label) + ': ' + String(item.count == null ? 0 : item.count));
});
return parts.length ? parts.join('. ') + '.' : fallback;
},
updateRepositoryMigration: function (button) {
var self = this;
var row = button.closest('[data-repository-row]');
if (!row || button.disabled) { return; }
var code = row.dataset.code || '';
var name = button.dataset.moduleName || code;
Adminx.Confirm.open({
kind: 'warning',
title: 'Применить миграции «' + name + '»?',
message: 'Файлы модуля уже актуальны. AVE.cms выполнит ещё не применённые миграции и синхронизирует версию БД.',
confirmLabel: 'Применить',
confirmClass: 'btn-primary',
onConfirm: function () {
button.disabled = true;
button.classList.add('is-loading');
self.post(self.base() + '/modules/lifecycle/' + encodeURIComponent(code) + '/update', new FormData()).then(function (result) {
button.disabled = false;
button.classList.remove('is-loading');
Adminx.Toast.show(result.message || (result.success ? 'Миграции применены' : 'Не удалось применить миграции'), result.success ? 'success' : 'error');
if (result.success) { window.setTimeout(function () { window.location.href = self.base() + '/modules?tab=catalog'; }, 350); }
});
}
});
},
openPresentation: function (button) {
var row = button.closest('[data-module-row]');
var drawer = document.getElementById('modulePresentationDrawer');
if (!row || !drawer) { return; }
var form = drawer.querySelector('[data-module-presentation-form]');
form.dataset.code = row.dataset.code || '';
drawer.querySelector('[data-module-presentation-title]').textContent = button.dataset.moduleName || 'Размещение модуля';
drawer.querySelector('[data-module-presentation-code]').textContent = row.dataset.code || '';
var headerOption = drawer.querySelector('[data-module-header-option]');
var dashboardOption = drawer.querySelector('[data-module-dashboard-option]');
headerOption.hidden = button.dataset.hasHeader !== '1';
dashboardOption.hidden = button.dataset.hasDashboard !== '1';
headerOption.querySelector('input').checked = button.dataset.headerEnabled === '1';
dashboardOption.querySelector('input').checked = button.dataset.dashboardEnabled === '1';
Adminx.Drawer.open('modulePresentationDrawer');
},
savePresentation: function (event) {
event.preventDefault();
var self = this;
var form = event.currentTarget;
var code = form.dataset.code || '';
if (!code) { return; }
var submit = form.querySelector('[type="submit"]');
var data = new FormData(form);
data.set('header_enabled', form.elements.header_enabled.checked ? '1' : '0');
data.set('dashboard_enabled', form.elements.dashboard_enabled.checked ? '1' : '0');
submit.disabled = true;
this.post(this.base() + '/modules/lifecycle/' + encodeURIComponent(code) + '/presentation', data).then(function (result) {
submit.disabled = false;
if (!result.success) {
Adminx.Toast.show(result.message || 'Не удалось сохранить размещение', 'error');
return;
}
Adminx.Toast.show(result.message || 'Размещение сохранено', 'success');
Adminx.Drawer.close('modulePresentationDrawer');
self.reloadSoon();
});
},
saveRepository: function (event) {
event.preventDefault();
var self = this;
var form = event.currentTarget;
var submit = form.querySelector('[type="submit"]');
var data = new FormData(form);
data.set('enabled', form.elements.enabled.checked ? '1' : '0');
submit.disabled = true;
this.post(this.base() + '/modules/repository/settings', data).then(function (result) {
submit.disabled = false;
if (!result.success) {
Adminx.Toast.show(result.message || 'Не удалось сохранить источник', 'error');
return;
}
Adminx.Toast.show(result.message || 'Источник модулей сохранён', 'success');
Adminx.Drawer.close('moduleRepositoryDrawer');
self.reloadSoon();
});
},
restoreOfficialRepository: function (button) {
var self = this;
Adminx.Confirm.open({
kind: 'warning',
title: 'Восстановить официальный каталог?',
message: 'Текущий URL и публичный ключ модулей будут заменены данными из доверенного профиля сборки.',
confirmLabel: 'Восстановить',
confirmClass: 'btn-primary',
onConfirm: function () {
button.disabled = true;
self.post(self.base() + '/modules/repository/settings/official', new FormData()).then(function (result) {
button.disabled = false;
Adminx.Toast.show(result.message || (result.success ? 'Официальный каталог восстановлен' : 'Не удалось восстановить каталог'), result.success ? 'success' : 'error');
if (result.success) { self.reloadSoon(); }
});
}
});
},
refreshRepository: function (button) {
var self = this;
if (button.disabled) { return; }
button.disabled = true;
button.classList.add('is-loading');
this.post(this.base() + '/modules/repository/refresh', new FormData()).then(function (result) {
button.disabled = false;
button.classList.remove('is-loading');
if (!result.success) {
Adminx.Toast.show(result.message || 'Не удалось обновить каталог', 'error');
return;
}
Adminx.Toast.show(result.message || 'Каталог обновлён', 'success');
self.reloadSoon();
});
},
installRepository: function (button) {
var self = this;
var row = button.closest('[data-repository-row]');
if (!row || button.disabled) { return; }
var code = row.dataset.code || '';
var name = button.dataset.moduleName || code;
var updating = button.dataset.moduleOperation === 'update';
Adminx.Confirm.open({
kind: updating ? 'warning' : 'info',
title: (updating ? 'Обновить «' : 'Установить «') + name + '»?',
message: updating
? 'AVE.cms скачает подписанный ZIP, проверит контрольные суммы и безопасно заменит файлы модуля. Если новой версии нужны миграции, они появятся отдельным следующим действием.'
: 'AVE.cms скачает подписанный ZIP, проверит контрольные суммы, добавит файлы и выполнит миграции модуля.',
confirmLabel: updating ? 'Обновить файлы' : 'Установить',
confirmClass: 'btn-primary',
onConfirm: function () {
button.disabled = true;
button.classList.add('is-loading');
self.post(self.base() + '/modules/repository/' + encodeURIComponent(code) + '/install', new FormData()).then(function (result) {
button.disabled = false;
button.classList.remove('is-loading');
if (!result.success) {
Adminx.Toast.show(result.message || (updating ? 'Не удалось обновить модуль' : 'Не удалось установить модуль'), 'error');
return;
}
Adminx.Toast.show(result.message || (updating ? 'Файлы модуля обновлены' : 'Модуль установлен'), 'success');
window.setTimeout(function () { window.location.href = result.redirect || (Adminx.base() + '/modules'); }, 350);
});
}
});
},
initArchive: function () {
var self = this;
var form = document.querySelector('[data-module-archive-form]');
if (!form) { return; }
var drop = form.querySelector('[data-module-archive-drop]');
var input = form.querySelector('[data-module-archive-input]');
var dragDepth = 0;
input.addEventListener('change', function () { self.selectArchive(form, input.files && input.files[0]); });
drop.addEventListener('dragenter', function (event) {
event.preventDefault();
dragDepth++;
drop.classList.add('is-dragging');
});
drop.addEventListener('dragover', function (event) { event.preventDefault(); });
drop.addEventListener('dragleave', function (event) {
event.preventDefault();
dragDepth = Math.max(0, dragDepth - 1);
if (!dragDepth) { drop.classList.remove('is-dragging'); }
});
drop.addEventListener('drop', function (event) {
event.preventDefault();
dragDepth = 0;
drop.classList.remove('is-dragging');
self.selectArchive(form, event.dataTransfer && event.dataTransfer.files ? event.dataTransfer.files[0] : null);
});
form.addEventListener('submit', function (event) { self.installArchive(event); });
},
selectArchive: function (form, file) {
var title = form.querySelector('[data-module-archive-title]');
var meta = form.querySelector('[data-module-archive-meta]');
var submit = form.querySelector('[data-module-archive-submit]');
form._moduleArchive = null;
if (!file) {
title.textContent = 'Перетащите ZIP сюда';
meta.textContent = 'или нажмите, чтобы выбрать файл до 25 МБ';
submit.disabled = true;
return;
}
if (!/\.zip$/i.test(file.name || '')) {
Adminx.Toast.show('Выберите ZIP-архив модуля', 'error');
this.selectArchive(form, null);
return;
}
if (file.size > 25 * 1024 * 1024) {
Adminx.Toast.show('ZIP-архив превышает 25 МБ', 'error');
this.selectArchive(form, null);
return;
}
form._moduleArchive = file;
title.textContent = file.name;
meta.textContent = this.fileSize(file.size) + ' · готов к проверке и установке';
submit.disabled = false;
},
installArchive: function (event) {
event.preventDefault();
var form = event.currentTarget;
if (form.getAttribute('aria-busy') === 'true') { return; }
var file = form._moduleArchive || (form.elements.module_archive.files && form.elements.module_archive.files[0]);
if (!file) { this.selectArchive(form, null); return; }
var submit = form.querySelector('[data-module-archive-submit]');
var progress = form.querySelector('[data-module-archive-progress]');
var progressTitle = form.querySelector('[data-module-archive-progress-title]');
var data = new FormData();
data.append('module_archive', file, file.name);
form.setAttribute('aria-busy', 'true');
form.classList.add('is-installing');
submit.disabled = true;
submit.classList.add('is-loading');
if (progress) { progress.hidden = false; }
if (progressTitle) { progressTitle.textContent = 'Устанавливаем ' + file.name; }
this.post(this.base() + '/modules/archive/install', data).then(function (result) {
if (!result.success) {
form.removeAttribute('aria-busy');
form.classList.remove('is-installing');
submit.disabled = false;
submit.classList.remove('is-loading');
if (progress) { progress.hidden = true; }
Adminx.Toast.show(result.message || 'Не удалось установить модуль', 'error');
return;
}
if (progressTitle) { progressTitle.textContent = 'Модуль установлен'; }
Adminx.Toast.show(result.message || 'Модуль установлен', 'success');
window.setTimeout(function () { window.location.href = result.redirect || (Adminx.base() + '/modules'); }, 350);
});
},
fileSize: function (bytes) {
if (bytes < 1024) { return bytes + ' Б'; }
if (bytes < 1024 * 1024) { return Math.round(bytes / 1024) + ' КБ'; }
return (bytes / 1024 / 1024).toFixed(1).replace('.0', '') + ' МБ';
},
reloadSoon: function () {
window.setTimeout(function () { window.location.reload(); }, 350);
}
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function () { Adminx.Modules.init(); });
} else {
Adminx.Modules.init();
}
})(window, document);