Skip to content

[Bug] Вкладка «Категории»: галки вложенных категорий слетают, повторное сохранение чистит ms3_product_categories #641

Description

@Ibochkarev

Описание проблемы

На вкладке товара «Категории» после перезагрузки страницы слетают галки вложенных дополнительных категорий. Строки в modx_ms3_product_categories ещё на месте: товар по-прежнему виден в этих разделах на сайте и в гриде. Если сохранить товар ещё раз, не ставя галки заново, таблица связей очищается и товар пропадает из дополнительных категорий.

Галки верхнего уровня дерева остаются. В дереве ещё показываются ресурсы, которые не являются msCategory.

Отчёт: modx.pro/help/25733 (MiniShop3 1.13.0-beta1, MODX 3.2.0, PHP 8.2).

Шаги воспроизведения

  1. Каталог с вложенностью, например КаталогРазделВнутренний раздел. Товар физически лежит в отдельной категории («Склад», modResource.parent).
  2. Открыть товар, вкладка «Категории».
  3. Отметить вложенные категории (не корень дерева). Сохранить.
  4. Проверить modx_ms3_product_categories: связи есть, товар виден в этих разделах на сайте и в админке.
  5. Обновить страницу редактирования товара (F5).
  6. Снова открыть «Категории»: галок у вложенных разделов нет, у верхних есть.
  7. Сохранить товар ещё раз, галки не ставить.

Ожидаемое поведение

После F5 все ранее выбранные дополнительные категории остаются отмеченными, в том числе вложенные. Повторное сохранение без изменений не трогает ms3_product_categories. В дереве выбираются категории магазина. Папки сайта для навигации не выглядят как категории с галкой.

Фактическое поведение

  • Вложенные галки пропадают сразу после reload, хотя строки в БД ещё есть.
  • Повторный save стирает связи: ProductCategoryMembershipWriter делает removeCollection и пишет insert из payload.
  • Верхний уровень дерева остаётся отмеченным.
  • В дереве видны обычные папки и документы MODX, не только msCategory.
  • Журнал ошибок пуст.

Скриншоты

Нет, отчёт с форума.

Окружение

  • MiniShop3: 1.13.0-beta1 (на текущем master та же Vue-вкладка #113 / ResourceCategoryTree)
  • MODX: 3.2.0
  • PHP: 8.2
  • MySQL: не указан
  • Браузер: не указан

Логи ошибок

Логи из core/cache/logs/error.log
пусто (по отчёту)

Дополнительный контекст

Почему так

Дерево ленивое: GET /api/mgr/product-data/{id}/categories/tree отдаёт только детей переданного parent. Первый запрос идёт с parent=0 (корни).

Как теряются id:

  1. ProductCategoriesTab берёт selectedIds из record.categories (все id из msProductData::get('categories'), включая вложенные).
  2. ResourceCategoryTree стартует с пустым checkedSet. Watch на modelValue без immediate: true, поэтому начальный список в сет не копируется.
  3. onMountedloadRoot()fetchChildren(0). В ответе только узлы верхнего уровня.
  4. mergeCheckedFromApi добавляет в сет только checked из этой страницы дерева.
  5. ensureLockedChecked() всегда вызывает emitSelection(). В v-model и hidden name="categories" остаются только видимые узлы плюс locked parent.
  6. Вложенные id выкинуты ещё до клика. После F5 UI уже не совпадает с БД.
  7. Save читает усечённый JSON. ProductCategoryMembershipWriter::saveCategories() удаляет все msCategoryMember и пишет только то, что пришло.

PHP это усиливает. Клиент всегда шлёт categories (даже []), поэтому $clientSentCategories === true и флаг checked не берётся из JOIN на msCategoryMember, только из preChecked. После усечённого emit разворот вложенного узла тоже не восстановит галку из БД.

Корневые галки остаются, потому что эти узлы есть в первом ответе parent=0.

Почему в дереве «не категории»

Так устроен ResourceCategoryTreeQueryTrait:

  • msCategory видимы и выбираемы.
  • modDocument / modWebLink / modResource с isfolder=1 видимы как навигация (без чекбокса, italic).

Старый ExtJS Processors/Category/GetNodes тоже показывал папки, но фильтровал context_key и опирался на defaultRootId. Vue-эндпойнт контекст не фильтрует и стартует с parent=0 по всем контекстам. В дереве оказываются Home, блог и прочие контейнеры.

Как чинить

Vue, vueManager/src/components/ResourceCategoryTree.vue. Выбор категорий не должен равняться загруженной странице дерева.

  • Инициализировать checkedSet из props.modelValue + lockedIds до loadRoot (immediate watch или явный seed).
  • После fetch не эмитить усечённый список. emitSelection() только по действию пользователя (toggle / bulk / снятие).
  • mergeCheckedFromApi делает union в сет, не замену. ensureLockedChecked только добавляет locked id.
  • Hidden categories в ProductCategoriesTab всегда сериализует полный selectedIds, а не то, что сейчас в DOM.

Регрессионный Vitest: modelValue = [parent, nestedDeep], mock корня без nestedDeep. После loadRoot в modelValue по-прежнему есть nestedDeep.

PHP, ProductCategoryTreeService / ProductDataController::getCategoriesTree. Наличие ключа categories в GET не значит, что клиент полностью задал выбор. Либо слать параметр только после dirty, либо на первой загрузке OR с Member. JOIN на msCategoryMember на первой загрузке вкладки игнорировать не стоит.

saveCategories лучше не менять на «не удалять то, чего нет в payload»: тогда нельзя снять галку. Чинить UI-контракт, не writer.

Состав дерева:

  • Фильтровать context_key товара, как GetNodes.
  • Корень: defaultRootId / site start, не все parent=0 всех контекстов.
  • Навигационные контейнеры оставить без чекбокса (уже так) и визуально отличить. По желанию не показывать контейнер без потомка-msCategory.
  • msCategory с show_in_tree=0 сейчас отфильтрованы. Если такие используют как доп. категории, галку в дереве не поставить. Это отдельная задача.

Проверки: Vitest на усечение selection в ResourceCategoryTree. PHPUnit/smoke: дерево вложенного member с полным categories[] отдаёт checked: true, writer по-прежнему wipe+insert. Ручной сценарий из шагов выше на 1.13+ / текущий master.

Связанный код: #113 / #479 (Vue-вкладка), #546 (цикл watch, поэтому emit после loadRoot опасен), ProductCategoryMembershipWriter, ResourceCategoryTreeQueryTrait.

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions