Олег Захаров

Олег Захаров

С нами с 14 октября 2017; Место в рейтинге пользователей: #64
2 часа назад
0
Сделал новую версию с табами и возможностью запуска сразу для всех вариантов.
Сначала содержимое для технического ресурса откуда будет запускаться выполнение сниппета.
[[!versionCleanXTabs? &maxVersions=`2` &types=`chunk,resource,template,snippet,plugin,templatevar` &dryRun=`0` &optimize=`1`]]
Далее содержимое versionCleanXTabs:
— в описании Tabbed wrapper for versionCleanX cleanup results.
— в код:
<?php
/**
 * versionCleanXTabs
 *
 * Runs versionCleanX for several VersionX entity tables and shows results in tabs.
 *
 * PROPERTIES:
 * &types - comma-separated list: chunk,resource,template,snippet,plugin,templatevar
 * &maxVersions - integer max versions to keep per element
 * &dryRun - 1 to show what would be deleted without deleting rows
 * &optimize - 1 to run OPTIMIZE TABLE in versionCleanX
 */

$escape = function ($value) {
    return htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
};

$toBool = function ($value) {
    return filter_var($value, FILTER_VALIDATE_BOOLEAN);
};

$typesRaw = (string)$modx->getOption('types', $scriptProperties, 'chunk,resource,template,snippet,plugin,templatevar');
$maxVersions = (int)$modx->getOption('maxVersions', $scriptProperties, 2);
$dryRun = $toBool($modx->getOption('dryRun', $scriptProperties, false));
$optimize = $toBool($modx->getOption('optimize', $scriptProperties, true));

$labels = [
    'chunk' => ['Чанки', 'versionx_chunk'],
    'resource' => ['Ресурсы', 'versionx_resource'],
    'template' => ['Шаблоны', 'versionx_template'],
    'snippet' => ['Сниппеты', 'versionx_snippet'],
    'plugin' => ['Плагины', 'versionx_plugin'],
    'templatevar' => ['TV-поля', 'versionx_templatevar'],
];

$types = [];
foreach (preg_split('/\s*,\s*/', $typesRaw, -1, PREG_SPLIT_NO_EMPTY) as $type) {
    $type = strtolower(trim($type));
    if (isset($labels[$type]) && !in_array($type, $types, true)) {
        $types[] = $type;
    }
}

if ($maxVersions < 1) {
    $maxVersions = 2;
}

if (empty($types)) {
    return '<p>versionCleanXTabs: не передан ни один поддерживаемый тип VersionX.</p>';
}

$uid = 'vcx-tabs-' . substr(md5(uniqid('', true)), 0, 10);

$css = <<<'VCX_STYLE'
<style>
.vcx-tool {
    max-width: 1180px;
    margin: 32px auto;
    padding: 0 18px 40px;
    color: #172033;
    font-family: Arial, sans-serif;
}
.vcx-tool h1 {
    margin: 0 0 12px;
    font-size: 34px;
    line-height: 1.15;
    font-weight: 600;
}
.vcx-tool__lead {
    max-width: 860px;
    margin: 0 0 22px;
    color: #536071;
    font-size: 16px;
    line-height: 1.55;
}
.vcx-tabs__nav {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin: 24px 0 0;
    border-bottom: 1px solid #d8dee8;
}
.vcx-tabs__button {
    appearance: none;
    border: 1px solid #d8dee8;
    border-bottom: 0;
    border-radius: 8px 8px 0 0;
    background: #f4f7fb;
    color: #1b2a41;
    padding: 11px 15px 10px;
    min-width: 132px;
    text-align: left;
    cursor: pointer;
}
.vcx-tabs__button small {
    display: block;
    margin-top: 2px;
    color: #6b7585;
    font-size: 11px;
}
.vcx-tabs__button.is-active {
    background: #fff;
    color: #00458f;
    border-color: #c5d1df;
}
.vcx-tabs__panel {
    display: none;
    padding: 24px 0 0;
}
.vcx-tabs__panel.is-active {
    display: block;
}
.vcx-result {
    overflow-x: auto;
}
.vcx-result h3 {
    margin-top: 0;
}
.vcx-result table {
    width: 100%;
    border-collapse: collapse;
    margin-top: 14px;
}
.vcx-result th,
.vcx-result td {
    padding: 9px 10px;
    border-bottom: 1px solid #e5e9f0;
    text-align: left;
    vertical-align: top;
}
.vcx-result th {
    background: #f5f7fa;
    font-weight: 600;
}
.vcx-tool__note {
    margin-top: 18px;
    padding: 12px 14px;
    border-left: 4px solid #00458f;
    background: #f3f7fc;
    color: #3d4a5c;
}
@media (max-width: 680px) {
    .vcx-tool h1 {
        font-size: 27px;
    }
    .vcx-tabs__button {
        flex: 1 1 100%;
    }
}
</style>
VCX_STYLE;

$out = $css;
$out .= '<section class="vcx-tool" id="' . $escape($uid) . '">';
$out .= '<h1>VersionX cleanup</h1>';
$out .= '<p class="vcx-tool__lead">Служебная страница очистки промежуточных версий VersionX. При открытии страницы запускается очистка всех вкладок: остаются только ' . (int)$maxVersions . ' последние версии для выбранных сущностей.</p>';
if ($dryRun) {
    $out .= '<p class="vcx-tool__note"><strong>Dry run:</strong> записи не удаляются, показан только расчет.</p>';
}

$out .= '<div class="vcx-tabs">';
$out .= '<div class="vcx-tabs__nav" role="tablist" aria-label="VersionX cleanup tabs">';
foreach ($types as $index => $type) {
    $panelId = $uid . '-' . $type;
    $active = $index === 0 ? ' is-active' : '';
    $out .= '<button class="vcx-tabs__button' . $active . '" type="button" role="tab" aria-selected="' . ($index === 0 ? 'true' : 'false') . '" data-vcx-tab="' . $escape($panelId) . '">';
    $out .= '<span>' . $escape($labels[$type][0]) . '</span><small>_' . $escape($labels[$type][1]) . '</small>';
    $out .= '</button>';
}
$out .= '</div>';

foreach ($types as $index => $type) {
    $panelId = $uid . '-' . $type;
    $active = $index === 0 ? ' is-active' : '';
    $result = (string)$modx->runSnippet('versionCleanX', [
        'contentType' => $type,
        'maxVersions' => $maxVersions,
        'dryRun' => $dryRun ? 1 : 0,
        'optimize' => $optimize ? 1 : 0,
    ]);

    if ($result === '') {
        $result = '<p>Сниппет versionCleanX не вернул результат для типа ' . $escape($type) . '.</p>';
    }

    $out .= '<div class="vcx-tabs__panel' . $active . '" id="' . $escape($panelId) . '" role="tabpanel">';
    $out .= '<div class="vcx-result">' . $result . '</div>';
    $out .= '</div>';
}
$out .= '</div>';

$out .= <<<'VCX_SCRIPT'
<script>
(function () {
    var root = document.currentScript ? document.currentScript.closest('.vcx-tool') : null;
    if (!root) {
        return;
    }
    var buttons = root.querySelectorAll('[data-vcx-tab]');
    var panels = root.querySelectorAll('.vcx-tabs__panel');
    buttons.forEach(function (button) {
        button.addEventListener('click', function () {
            var targetId = button.getAttribute('data-vcx-tab');
            buttons.forEach(function (item) {
                var active = item === button;
                item.classList.toggle('is-active', active);
                item.setAttribute('aria-selected', active ? 'true' : 'false');
            });
            panels.forEach(function (panel) {
                panel.classList.toggle('is-active', panel.id === targetId);
            });
        });
    });
})();
</script>
VCX_SCRIPT;

$out .= '</section>';

return $out;
Далее содержимое сниппета versionCleanX (отличается от варианта выше):
<?php
ini_set('memory_limit', '256M');

/**
 * versionCleanX
 *
 * Cleans old VersionX rows while keeping the newest N versions per element.
 *
 * PROPERTIES:
 * &contentType - resource, chunk, plugin, snippet, template, or templatevar
 * &maxVersions - integer max versions to keep per element
 * &dryRun - 1 to show what would be deleted without deleting rows
 * &optimize - 1 to run OPTIMIZE TABLE after cleanup
 *
 * USAGE:
 * [[!versionCleanX? &contentType=`resource` &maxVersions=`10`]]
 */

$type = strtolower(trim((string)$modx->getOption('contentType', $scriptProperties, 'resource')));
$maxVersions = (int)$modx->getOption('maxVersions', $scriptProperties, 5);
$dryRun = (bool)$modx->getOption('dryRun', $scriptProperties, false);
$optimize = (bool)$modx->getOption('optimize', $scriptProperties, true);

$titleColumns = [
    'chunk' => 'name',
    'plugin' => 'name',
    'snippet' => 'name',
    'template' => 'templatename',
    'templatevar' => 'name',
    'resource' => 'title',
];

if (!array_key_exists($type, $titleColumns)) {
    $type = 'resource';
}

if ($maxVersions < 1) {
    return 'VersionX cleanup error: maxVersions must be greater than 0.';
}

$table = $modx->getOption('table_prefix') . 'versionx_' . $type;
$titleColumn = $titleColumns[$type];
$query = "SELECT version_id, content_id, {$titleColumn} AS page_title FROM `{$table}` ORDER BY content_id ASC, version_id DESC";

$stmt = $modx->query($query);
if (!is_object($stmt)) {
    return 'VersionX cleanup query error: ' . print_r($modx->errorInfo(), true);
}

$rowsHtml = '';
$total = 0;
$deleted = 0;
$currentContentId = null;
$currentTitle = '';
$currentTotal = 0;
$currentDeleted = 0;
$keptForCurrent = 0;

$flushRow = function () use (&$rowsHtml, &$currentContentId, &$currentTitle, &$currentTotal, &$currentDeleted) {
    if ($currentContentId === null) {
        return;
    }

    $rowsHtml .= '<tr><td>' . htmlspecialchars((string)$currentTitle, ENT_QUOTES, 'UTF-8') . '</td><td>' .
        (int)$currentTotal . '</td><td>' . (int)$currentDeleted . "</td></tr>\n";
};

while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
    $contentId = (int)$row['content_id'];

    if ($currentContentId !== $contentId) {
        $flushRow();
        $currentContentId = $contentId;
        $currentTitle = (string)$row['page_title'];
        $currentTotal = 0;
        $currentDeleted = 0;
        $keptForCurrent = 0;
    }

    $total++;
    $currentTotal++;

    if ($keptForCurrent < $maxVersions) {
        $keptForCurrent++;
        continue;
    }

    $versionId = (int)$row['version_id'];
    if (!$dryRun) {
        $delete = $modx->query("DELETE FROM `{$table}` WHERE version_id = {$versionId}");
        if (!is_object($delete)) {
            return 'VersionX cleanup delete error for ' . htmlspecialchars($currentTitle, ENT_QUOTES, 'UTF-8') .
                ': ' . print_r($modx->errorInfo(), true);
        }
    }

    $deleted++;
    $currentDeleted++;
}

$flushRow();

$optimizeMessage = '';
if (!$dryRun && $optimize) {
    $optimized = $modx->query("OPTIMIZE TABLE `{$table}`");
    if (!is_object($optimized)) {
        $optimizeMessage = '<p>Optimize error: ' . htmlspecialchars(print_r($modx->errorInfo(), true), ENT_QUOTES, 'UTF-8') . '</p>';
    }
}

return '<h3>VersionX Cleanup for ' . htmlspecialchars($table, ENT_QUOTES, 'UTF-8') . '</h3>' .
    ($dryRun ? '<p><strong>Dry run:</strong> rows were not deleted.</p>' : '') .
    '<p>Total records: <strong>' . (int)$total . '</strong>
Total deleted: <strong>' . (int)$deleted . '</strong></p>' .
    $optimizeMessage .
    '<table class="table table-striped"><thead><tr><th>Page name</th><th>Total found</th><th>Deleted</th></tr></thead><tbody>' .
    $rowsHtml .
    '</tbody></table>';
Итого имеем удобный вывод с вкладками.
<cut/>
Из статьи на моем сайте gowindo.ru/articles/modx/versioncleanxtabs-chistka-ot-ustarevshix-versij
13 мая 2026, 23:33
0
Сейчас в таких вопросах проще и быстрее спросить у ChatGPT. А лучше подключить себе Codex и работать с сайтом напрямую…
12 мая 2026, 21:07
0
Тоже в последнее время очень плотно работаю с различными проектами ИИ. Делал корпоративный ИИ на локальном сервере и VPS. VPS для VPN и чтобы Claude Code CLI ходил только через него (все-таки стремно потерять Claude Code MAXx20). ChatGPT Codex очень нравится — если его обучить, он все четко делает. А и еще Kimi K2.6 очень прикольно умеет осаждать другие ИИ и давать бизнесово оценку — мне из 10-ти пунктов от ChatGPT Codex в плане создания продукта Kimi оставил 3, остальные обоснованно по пальцам объяснил что не нужны на первом этапе — так и оказалось.
Ставил Sing-box и самописный kill-switcher — типа рубильник на интернет если VPN отвалится. Когда делал VPN заодно и как-то быстро и просто сделал прокси-сервер для Telegram — настолько ИИ уже научены делать его быстро — буквально наверное 2-3 минуты. Просто он сам предложил типа «А хочешь прокси-сервер для Telegram по протоколу Mtproto сделаю?».
Можно сделать разные VPN и прокси-сервера для своих сотрудников с возможностью отключения если уволились.
Сейчас тоже решил запилить себе ИИ-агента для создания сайтов на MODX Revo и других движках, самое сложное скиллы реализовать нужные. Kimi у меня на тесте хуже справился по сравнению с ChatGPT.
Удивился что кто-то уже реализовал ИИ на MODX (а впрочем чему удивляться — ИИ сейчас приведут к ускорению разработки сайтов в 10-20-100 раз.) MODX Revo тут хорош тем что по нему для ИИ все понятно и просто.
Но самое правильное — чтобы ИИ создавал скрипты на повторяющиеся операции, которые на ИИ делать накладно из-за расходов лимитов/токенов. Или делать структуру из нескольких ИИ. Но это если вы делаете сайты на потоке.
26 апреля 2026, 15:27
0
Я потому и задал вопрос о том как реализовано в Minishop3?
26 апреля 2026, 13:53
0
ms2_product_options как хранит опции товаров?
У меня на сайте рабочем на MODX REVO 2.8.8 + Minishop 3.0.7 заметил вот что —
для многих товаров требуется указание от 10 до 30 опций — по факту заполняют не все. Причем тут пришлось делать конфигуратор, а для него еще создать штук 20 опций.
А по факту у меня часто не все опции заполняются. И получается многие записи хранятся пустые. Т.е. на каждый товар создается запись вида product_id — key опции с пустым value. А можно же не хранить пустые опции? Не знаю почему и как, но вызывает сомнение что так надо было делать.
Всего на рабочем проекте будет более 100 000 товаров (будет больше).
В итоге в таблице ms2_product_options на текущий момент уже вбито 35 000 строк, а когда зальем все товары со всеми опциями, то там будет просто жесть как много строк с пустыми значениями.
11 апреля 2026, 17:19
0
Демо-сайт не работает.
Последнее сообщение аж от 2019 года!
Можно ли из старого заказа сделать повторную покупку — то есть добавить в корзину список товаров из заказа или отдельные его товары? Понимаю что можно — просто не совсем понимаю реализовано ли уже или мне придется допиливать под себя?
11 апреля 2026, 17:15
0
Добра всем!
По ходу компонент заброшен? Последнее сообщение аж от 2019 года? всего 26 загрузок.
Вопрос текущий:
Можно ли на этом компоненте сделать некие различные типы корзин типа список покупок, список желаний, спецификации (сметы) с указанием проекта. Можно ли реализовать возможность доступа к корзинам других пользователей в рамках управления пользователями в одной организации на базе Organizations?
Делаю конфигуратор сложных составных товаров. Надо сохранять расчеты (сметы) с возможностью передачи в закупку.
11 апреля 2026, 17:11
0
Добра всем!
По ходу компонент заброшен?
Удивился сам что я когда-то его тестил и писал по нему вопросы, уже не помню даже когда и где.
Вопрос текущий:
Можно ли на этом компоненте сделать некие различные типы корзин типа список покупок, список желаний, спецификации (сметы) с указанием проекта. Можно ли реализовать возможность доступа к корзинам других пользователей в рамках управления пользователями в одной организации на базе Organizations?
Делаю конфигуратор сложных составных товаров. Надо сохранять расчеты (сметы) с возможностью передачи в закупку.
11 апреля 2026, 17:08
0
Добра всем!
Не понял как создать несколько корзин?
И чтобы у каждого был свой шаблон.
Можно ли так сделать некие различные типы корзин типа список покупок, список желаний, спецификации (сметы) с указанием проекта. Можно ли реализовать возможность доступа к корзинам других пользователей в рамках управления пользователями в одной организации на базе Organizations?
Делаю конфигуратор сложных составных товаров. Надо сохранять расчеты (сметы) с возможностью передачи в закупку.
01 апреля 2026, 18:15
0
У меня на регистрации словил ошибку.
При это пользователь явно создался, т.к. повторно создать пользователя на ту же самую почту не дает.
При повторно попытке восстановить доступ так же ошибка выходит и кидает на страницу отладочную rumaxbot.ru/email/verification-notification.
На почту письмо о подтверждении не пришло.
21 февраля 2026, 01:17
0
По этому вопросу тоже думаю — создал вопрос тут
20 февраля 2026, 16:01
0
Компонент очень нужный и мне кажется будет востребован.
У меня тут задача стоит сделать что-то подобное на сайте на движке на MODX 2.8 — там есть старые другие решения.
Но задумываюсь о глобальном переносе на 3-ю версию.
20 февраля 2026, 16:00
0
ну я подумал что возможно ты сам лично уже где-то у себя на заказе рабочем внедрил и есть рабочий сайт
20 февраля 2026, 15:58
0
А какой компонент для личного кабинета и авторизации используется?
20 февраля 2026, 14:42
0
тут пришла мысль что никто не захочет просто так делиться своим опытом за бесплатно. Можно было бы сделать статьи и кейсы платными? Типа хочешь прочитать инструкцию или кейс по настройке того или иного компонента — ведь кто-то потратил время на написание инструкции — стоимость 100 руб.
Тут желательно конечно не переусердствовать. Но и обидно не будет тратить время на написание кейсов и инструкций.
А чтобы было понятно что больше всего интересует людей — было бы неплохо сделать список вопросов, на которые нужны ответы — типа голосование или например в описании компонентов добавить голосование за необходимость написания кейсов и готовность оплатить или сбор средств на написание инструкций. Понятно что есть стандартные инструкции, но они не всегда понятные.
И часто бывают брошенные компоненты. Можно было бы собирать средства на его дальнейшее развитие.
20 февраля 2026, 13:31
0
Можете прислать примеры живых сайтов не решении? На которых данное решение уже установлено?
30 апреля 2025, 16:35
0
$_modx->user->isAuthenticated() у меня не сработало в чанке через fenom — выдавало ошибку
В fenom правильно будет $_modx->isAuthenticated()
07 марта 2025, 23:17
0
Про код. Про «научиться писать код понятный и логичный». Не в тему поста выше, ситуация вчера/сегодня и смех и грех.
Вчера пришел запрос от Заказчика на сайте на Wordpress — сайт криптообменника. Там в настройках надо было добавить новый статус заказа. Добавили в админке, а на сайте у клиентов не отображается. Ну я типа ща минуту, легкотня.
В итоге провозился 2 дня. Пока разбирался в чужом коде. Вчера даже тестовый сайт лег после пробных правок кода, пришлось восстанавливать.
В итоге сегодня разобрался. Выставляю счет Заказчику за 7 часов работы анализа и правок и отладки кода (3 часа восстановления сайта не в счет, оказалось что сайт лог ошибок не пишет, а восстановить бэкап просто так нельзя и «вообще некогда нам» — кое как сам добавил логирование ошибок и восстановил сайт).
Заказчик в итоге спрашивает у меня — с какого так много запаришваешь? Типа поменять там всего то текст. А оказалось что в админке есть настройка, а в коде названия статусов выводит через дефолтные настройки в самом коде (рука лицо!). И вообще в админке указываемое название статуса нигде не используется. И 7 полей из 10 из таблицы статусов в БД не используется почти. И куча артефактов. И фильтров и функций непонятно каких. И вообще id статуса в таблице заказа не используется, а используется текстовое название статуса (аааа!). И вот сидишь разбираешься в этом г… не и не понятно кто будет платить за разбор. А казалось бы просто вывести название статуса в заказе клиента…
А до этого тоже делал подобное. И пока доделал — оказалось что задачка стала неактуальной. И код правки (по итогу 2 строчки кода) стал никому не нужен. Опять же 2 дня из-за непонятного чужого кода. Ладно хоть заплатили…
07 марта 2025, 22:34
0
Спасибо за статью.
Я столкнулся с проектом где часть админки начали делать на VUE.js потому что так захотел/обосновал предыдущий программист. А после его ухода заказчик остался с недоделанным личным кабинетом. А по факту там надо было реализовать подгрузку документов пользователя. Ну может еще что реактивного, но я задался вопросом почему нельзя было стандартными средствами MODX реализовать и с Ajax-запросами.
Теперь Заказчику приходится искать специалиста и по MODX и по VUE.