mirror of
https://github.com/avecms/AVE.cms.git
synced 2026-08-01 08:45:44 +00:00
457 lines
22 KiB
JavaScript
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);
|