Всего 125 909 комментариев

Дмитрий
Вчера в 12:32
0
Добрый день, в утилите Импорта отсутствует поле с остатком. В доках указано что должно работать с remains photo-screen.ru/i/GjgHIPlSI
Олег Захаров
19 мая 2026, 04:04
1
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
Сергей Карпович
18 мая 2026, 17:28
0
github.com/VolgaIgor/editorjs-gallery — как будто то что нужно, основан на editor-js/image, имеет настройки галереи сетка или слайдер, как я понимаю
Андрей Шевяков
18 мая 2026, 14:54
0
Эту строчку:
ym('XXXXXX','reachGoal','TARGET_NAME')
Надо поменять на вашу из Яндекс метрики.

Для MODX 2 и miniShop2 все должно работать.

Не будет ли такого что пользователь будет обновлять по нескольку раз страницу успешного заказа и событие будет каждый раз засчитываться?
Нет не будет, скрипт отправит цель в Метрику, один раз, при создании заказа и все.
Максим
18 мая 2026, 13:45
0
Сделал в ручную. эту базу. При установке вообще не каких баз не создает
Павел Гвоздь
18 мая 2026, 13:00
0
который поддерживает 3 CLI интерфейса opencode, claude, codex. Это я к тому, что можно сделать адаптер под CLI, чтобы при старте сессии он читал стандартный системный Промт, в котором написано прочитать твой универсальный Промт
Вроде в aiAssist ровно так… ну разве, что кроме поддержки OpenCode CLI (пока не требуется).

А что касается агента в телеге:

Это Fcklaw – мой ответ OpenClaw. 🫠
Он тоже кстати, как и aiAssist, спавнит Claude Code CLI и Codex CLI напрямую, без API ключей.
Олег
18 мая 2026, 12:36
0
UPD. В чанк Scripts вставил, событие не работает и не фиксируется метрикой. И ещё момент, как сделать так чтобы был один заказ — одно событие? Не будет ли такого что пользователь будет обновлять по нескольку раз страницу успешного заказа и событие будет каждый раз засчитываться?
Максим
18 мая 2026, 10:05
0
Это при установки, в итоге не создает полностью все базы…
Иван Бочкарев
18 мая 2026, 10:02
0
Добрый день!

Это на какой версии проявляется?
Иван Бочкарев
18 мая 2026, 09:58
0
Привет!

Проверь в новой версии (выложил в магазин)