Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

## Навигация

- **Текущий месяц:** [Апрель 2026](#апрель-2026) (ниже)
- **Предыдущий месяц:** [Март 2026](#март-2026) (ниже)
- **Ещё раньше:** [Февраль 2026](#февраль-2026), [Январь 2026](#январь-2026) (ниже)
- **Текущий месяц:** [Май 2026](#май-2026) (ниже)
- **Предыдущий месяц:** [Апрель 2026](#апрель-2026) (ниже)
- **Ещё раньше:** [Март 2026](#март-2026), [Февраль 2026](#февраль-2026), [Январь 2026](#январь-2026) (ниже)
- **Архив по месяцам:**
- [Декабрь 2025](changelogs/2025-12.md)
- [Ноябрь 2025](changelogs/2025-11.md)
Expand All @@ -15,6 +15,19 @@

---

## Май 2026

### [2026-05-08]

#### Исправлено

**Дерево категорий в UI привязки опций при отсутствии msCategory под корнем сайта (#237):**
- Добавлена системная настройка `ms3_option_category_tree_parent`: ID ресурса-родителя для первого уровня дерева в менеджере опций (0 — поведение по умолчанию, только прямые дети корня с `parent = 0`).
- `OptionsController::getTree` подставляет этот родитель при запросе с `parent = 0`; ленивая подгрузка вложенных уровней по реальным `id` не затрагивается.
- В `ms3.config` для страниц настроек и категории передаётся `optionCategoryTreeParent`; Vue (`OptionsGrid`, `CategoryOptionsTab`) использует его как `rootParent` дерева категорий.

---

## Апрель 2026

### [2026-04-27] 🚀 Версия 1.10.1-beta1
Expand Down
5 changes: 5 additions & 0 deletions _build/elements/settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@
'xtype' => 'combo-boolean',
'area' => 'ms3_category',
],
'ms3_option_category_tree_parent' => [
'value' => 0,
'xtype' => 'numberfield',
'area' => 'ms3_options',
],
'ms3_template_category_default' => [
'value' => '',
'xtype' => 'modx-combo-template',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ public function loadCustomCssJs()
'connector_url' => $this->ms3->config['connectorUrl'],
'show_options' => $showOptions,
'default_thumb' => $this->ms3->config['defaultThumb'],
'optionCategoryTreeParent' => (int)$this->modx->getOption('ms3_option_category_tree_parent', null, 0),
'show_nested_products' => (bool) $this->modx->getOption('ms3_category_show_nested_products', null, true),
'category_products_rows' => (int) $this->getOption('ms3_category_products_default_rows', null, 20),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ public function loadCustomCssJs()

$config = $this->ms3->config;
$config['default_thumb'] = $this->ms3->config['defaultThumb'];
$config['optionCategoryTreeParent'] = (int)$this->modx->getOption('ms3_option_category_tree_parent', null, 0);

$this->addHtml('<script>
ms3.config = ' . json_encode($config) . ';
Expand Down
3 changes: 3 additions & 0 deletions core/components/minishop3/lexicon/en/setting.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
$_lang['area_ms3_security'] = 'Security';
$_lang['area_ms3_api'] = 'API';
$_lang['area_ms3_notifications'] = 'Notifications';
$_lang['area_ms3_options'] = 'Product options';

$_lang['setting_ms3_chunks_categories'] = 'Categories for chunks list';
$_lang['setting_ms3_chunks_categories_desc'] = 'Comma-separated list of category IDs for chunks list.';
Expand Down Expand Up @@ -56,6 +57,8 @@
$_lang['setting_ms3_category_show_nested_products_desc'] = 'If you enable this option, all nested products will be shown in category. They are highlighted with different color and have their parent category name under pagetitle.';
$_lang['setting_ms3_category_show_options'] = 'Show category product options';
$_lang['setting_ms3_category_show_options_desc'] = 'Show options for category products.';
$_lang['setting_ms3_option_category_tree_parent'] = 'Options UI: category tree root resource ID';
$_lang['setting_ms3_option_category_tree_parent_desc'] = 'When product categories (msCategory) are not direct children of the site root (parent = 0), set this to the container resource ID under which categories live. The manager options tree and category pickers then load the first level relative to this parent. Use 0 for default behavior (only msCategory resources with parent = 0).';
$_lang['setting_ms3_category_remember_grid'] = 'Remember category grid';
$_lang['setting_ms3_category_remember_grid_desc'] = 'If enabled, category grid state will be remembered and restored on page load, including page number and search string.';
$_lang['setting_ms3_category_id_as_alias'] = 'Category id as alias';
Expand Down
3 changes: 3 additions & 0 deletions core/components/minishop3/lexicon/ru/setting.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
$_lang['area_ms3_security'] = 'Безопасность';
$_lang['area_ms3_api'] = 'API';
$_lang['area_ms3_notifications'] = 'Уведомления';
$_lang['area_ms3_options'] = 'Опции товаров';

$_lang['setting_ms3_chunks_categories'] = 'Категории для списка чанков';
$_lang['setting_ms3_chunks_categories_desc'] = 'Список ID категорий через запятую для списка чанков.';
Expand Down Expand Up @@ -56,6 +57,8 @@
$_lang['setting_ms3_category_show_nested_products_desc'] = 'Если вы включаете эту опцию, то в категории будут показаны все вложенные товары. Они выделены другим цветом и у них есть имя родной категории под pagetitle.';
$_lang['setting_ms3_category_show_options'] = 'Показывать опции товаров категории';
$_lang['setting_ms3_category_show_options_desc'] = 'Показывать опции к товарам категории.';
$_lang['setting_ms3_option_category_tree_parent'] = 'UI опций: ID ресурса-корня дерева категорий';
$_lang['setting_ms3_option_category_tree_parent_desc'] = 'Если категории магазина (msCategory) не являются прямыми детьми корня сайта (parent = 0), укажите ID контейнерного ресурса, под которым они лежат. Тогда дерево категорий в настройках опций и селекторах загрузит первый уровень относительно этого родителя. 0 — только категории с parent = 0 (поведение по умолчанию).';
$_lang['setting_ms3_category_remember_grid'] = 'Запоминание таблицы категории';
$_lang['setting_ms3_category_remember_grid_desc'] = 'Если включено, состояние таблицы категории будет запоминаться и восстанавливаться при загрузке страницы, включая номер страницы и строку поиска.';
$_lang['setting_ms3_category_id_as_alias'] = 'Id категории как псевдоним';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -306,11 +306,13 @@ public function getTypes(): array
* MODX resource tree for category selection. Optional ?option_id= to flag which
* categories already have the option linked (for checkbox UI).
*
* @param array $params parent (default 0), option_id (optional), categories[] (prechecked)
* @param array $params parent (default 0 = semantic root; resolved via ms3_option_category_tree_parent when set),
* option_id (optional), categories[] (prechecked)
*/
public function getTree(array $params = []): array
{
$parent = (int)($params['parent'] ?? 0);
$requestedParent = (int)($params['parent'] ?? 0);
$parent = $this->resolveOptionsTreeParentId($requestedParent);
$optionId = isset($params['option_id']) ? (int)$params['option_id'] : 0;
$preChecked = $this->decodeIntArray($params['categories'] ?? null);

Expand Down Expand Up @@ -559,6 +561,20 @@ protected function splitCategoriesPayload($payload): array
return [$enabled, $disabled];
}

/**
* Map semantic tree root (client sends parent=0) to real MODX resource parent id when
* msCategory resources are not direct children of site root.
*/
private function resolveOptionsTreeParentId(int $requestedParent): int
{
if ($requestedParent !== 0) {
return $requestedParent;
}
$configured = (int)$this->modx->getOption('ms3_option_category_tree_parent', null, 0);

return $configured > 0 ? $configured : 0;
}

/**
* Decode ids array whether it came as JSON string, comma string, or array.
*
Expand Down
7 changes: 5 additions & 2 deletions vueManager/src/components/CategoryOptionsTab.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ import Select from 'primevue/select'
import Toast from 'primevue/toast'
import { useConfirm } from 'primevue/useconfirm'
import { useToast } from 'primevue/usetoast'
import { onMounted, ref, watch } from 'vue'
import { computed, onMounted, ref, watch } from 'vue'

import request from '../request.js'
import { getMs3Config } from '../utils/modx.js'

const props = defineProps({
categoryId: { type: Number, required: true },
Expand All @@ -25,6 +26,8 @@ const toast = useToast()
const confirm = useConfirm()
const { _ } = useLexicon()

const optionCategoryTreeRootParent = computed(() => getMs3Config()?.optionCategoryTreeParent ?? 0)

const links = ref([])
const loading = ref(false)
const searchQuery = ref('')
Expand Down Expand Up @@ -86,7 +89,7 @@ async function loadAvailableCategories() {
// flattened: just fetch the first level of resources for now (limit to obvious set).
// A richer picker would use the full tree, but a flat Select is consistent with the
// legacy ExtJS behavior.
const r = await request.get('/api/mgr/options/tree', { parent: 0 })
const r = await request.get('/api/mgr/options/tree', { parent: optionCategoryTreeRootParent.value })
const roots = r?.results || []
// Expand one level for convenience (walk all children of each msCategory root).
const expanded = [...roots]
Expand Down
2 changes: 1 addition & 1 deletion vueManager/src/components/OptionCategoryTree.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const props = defineProps({
modelValue: { type: Array, default: () => [] },
/** Pre-fetch checked state from the option's current links. Pass 0/null to skip. */
optionId: { type: [Number, String], default: 0 },
/** Root resource id. 0 = site root. */
/** Root resource id for first tree request. 0 = semantic site root (resolved on API via ms3_option_category_tree_parent when set). */
rootParent: { type: Number, default: 0 },
})

Expand Down
13 changes: 10 additions & 3 deletions vueManager/src/components/OptionsGrid.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,16 @@ import { useToast } from 'primevue/usetoast'
import { computed, onMounted, ref, watch } from 'vue'

import request from '../request.js'
import { getMs3Config } from '../utils/modx.js'
import OptionCategoryTree from './OptionCategoryTree.vue'
import OptionValuesEditor from './OptionValuesEditor.vue'

const toast = useToast()
const confirm = useConfirm()
const { _ } = useLexicon()

const optionCategoryTreeRootParent = computed(() => getMs3Config()?.optionCategoryTreeParent ?? 0)

// Grid state
const options = ref([])
const totalRecords = ref(0)
Expand Down Expand Up @@ -333,7 +336,7 @@ onMounted(() => {
<!-- Category filter tree (left pane) -->
<aside class="options-tree-pane">
<h4 class="pane-title">{{ _('ms3_categories') || 'Категории' }}</h4>
<OptionCategoryTree v-model="selectedCategories" />
<OptionCategoryTree v-model="selectedCategories" :root-parent="optionCategoryTreeRootParent" />
</aside>

<!-- Main grid + toolbar + dialog (right pane) -->
Expand Down Expand Up @@ -527,7 +530,11 @@ onMounted(() => {

<div class="dialog-tree">
<h4 class="pane-title">{{ _('ms3_categories') || 'Категории' }}</h4>
<OptionCategoryTree v-model="editingCategories" :option-id="editing.id || 0" />
<OptionCategoryTree
v-model="editingCategories"
:option-id="editing.id || 0"
:root-parent="optionCategoryTreeRootParent"
/>
</div>
</div>

Expand Down Expand Up @@ -557,7 +564,7 @@ onMounted(() => {
'Выберите категории — в них будут созданы связи для выбранных опций (существующие останутся).'
}}
</p>
<OptionCategoryTree v-model="assignCategories" />
<OptionCategoryTree v-model="assignCategories" :root-parent="optionCategoryTreeRootParent" />
<template #footer>
<Button
:label="_('cancel') || 'Отмена'"
Expand Down