Skip to content

feat(options): full Vue migration of options management + PR #200 integration#205

Merged
biz87 merged 30 commits intobetafrom
feat/options-vue-refactor
Apr 20, 2026
Merged

feat(options): full Vue migration of options management + PR #200 integration#205
biz87 merged 30 commits intobetafrom
feat/options-vue-refactor

Conversation

@biz87
Copy link
Copy Markdown
Member

@biz87 biz87 commented Apr 20, 2026

Summary

Полный перевод управления опциями товара с ExtJS на Vue + REST API. Удалено ~2600 строк legacy-кода (7 ExtJS файлов и 22 PHP-процессора). Также перенесена суть #203 (Issue #200) — per-category caption/description override — поверх новой Vue-архитектуры.

Что поменялось

Backend

  • REST-контроллеры OptionsController и CategoryOptionsController (+ роуты /api/mgr/options/* и /api/mgr/categories/{id}/options/*) вместо 22 MODX-процессоров.
  • Declarative msOptionType::getSchema() — type handlers отдают array вместо JS-строк.
  • Миграция добавляет caption/description на ms3_category_options + overlay-логика в OptionLoaderService (из PR feat(options): per-category caption and description for category-option links #203).
  • Фикс convertPreloadedValue: case-insensitive match + 3 multi-type (comboMultiple/Colors/Options) — раньше только ComboMultiple строго матчился, reload терял все значения кроме первого.
  • Hidden input с JSON-массивом для multi-value опций товара (Utils::decodeOptionValue()) — обход ограничения ExtJS BasicForm.getValues(), который читал только последний DOM-input при нескольких с одинаковым name.

Frontend

  • Настройки → Опции: OptionsGrid.vue + OptionCategoryTree.vue (дерево категорий MODX с независимыми чекбоксами и контекстным меню: обновить/развернуть/свернуть ветку/выделить все вложенные/снять все) + OptionValuesEditor.vue (редактор значений с drag-drop и PrimeVue ColorPicker).
  • Категория товара → Опции: CategoryOptionsTab.vue с inline-редактором, drag-drop сортировкой, массовыми действиями, диалогами «Добавить опцию» и «Копировать опции из другой категории». Колонки Глобально / Название (для категории) для override из PR feat(options): per-category caption and description for category-option links #203.
  • Карточка товара → Опции: ProductOptionField.vue + ProductOptionsTab.vue для всех 10 типов опций. comboOptions → PrimeVue InputChips (Enter / запятая / blur = коммит тега).

Удалено (2600 строк)

  • assets/.../js/mgr/settings/option/* (grid, window, tree, types/*)
  • assets/.../js/mgr/category/option.grid.js, option.windows.js
  • src/Processors/Settings/Option/*.php (11 файлов)
  • src/Processors/Category/Option/*.php (11 файлов)

Test plan

  • Настройки → Опции: создание / редактирование всех 10 типов (textfield, numberfield, textarea, checkbox, comboBoolean, combobox, comboMultiple, comboColors, comboOptions, datefield)
  • Значения для combobox / comboMultiple / comboColors — редактор с drag-drop + ColorPicker
  • Дерево категорий MODX — независимые чекбоксы, контекстное меню, фильтр только msCategory, leaf по COUNT(Child)
  • Массовое назначение опций в категории
  • Категория → Опции: drag-drop, inline-edit value, массовые действия (activate/deactivate/require/unrequire/remove)
  • Копирование опций из другой категории
  • Карточка товара → Опции: сохранение всех типов, multi-value сохраняются полностью (все выбранные значения)
  • comboOptions: freeform ввод (Enter/запятая/blur), suggestion-endpoint
  • Per-category caption/description override — inline edit в гриде категории, диалог добавления, колонка «Глобально» для сравнения
  • PHPStan: мои файлы 0 ошибок (1 pre-existing false-positive из PR feat(options): per-category caption and description for category-option links #203 overlay)

Связанные issues/PRs

🤖 Generated with Claude Code

…ductOptionsTab

Replaces ExtJS mount in ProductTabs options tab (Ext.create(modx-vtabs) + ms3.utils.getExtField)
with pure Vue components:

- ProductOptionField.vue — renders a single option field by msOption.type
  (textfield, numberfield, textarea, checkbox, comboBoolean, combobox,
  comboMultiple, comboColors, comboOptions, datefield). Hidden inputs
  propagate values into the MODX form POST as options-{key}[.options-{key}[]]
  so the existing Product/Create and Product/Update processors work unchanged.

- ProductOptionsTab.vue — vertical navigation grouping options by msOption.modcategory_id
  (category_name as title). Flat list if only one group. Non-scoped .vueApp CSS to
  avoid Vite scoped-hash mismatch across chunks.

- ProductTabs.vue — swap type: 'extjs-options' to type: 'vue', drop mountOptionsTab()
  and related mount/destroy/CSS. No more reliance on Ext.create or ms3.utils.getExtField
  for the options tab.
Adds msOptionType::getSchema($field): array as a parallel to getField() (which returns
ExtJS-specific JS string). Vue renderer consumes schema to build PrimeVue components
without touching Ext.*.

Default implementation derives type key from class short name (ComboBoolean → comboBoolean)
and passes msOption.properties through as-is. Individual Types/*.php can override for richer
schema (e.g. normalized values list).

Also:
- msOption::getSchema() delegates to type handler (mirrors getManagerField()).
- OptionLoaderService::getFieldsForProduct() attaches schema to each option_fields row
  alongside existing ext_field, so Vue consumers can use schema while ExtJS form (if still
  present anywhere) keeps working via ext_field.

getField() is intentionally kept — will be removed in the cleanup stage of this refactor.
…onsController

Replaces legacy Processors/Settings/Option/* and Processors/Category/Option/* with
two REST controllers that delegate business logic to existing services
(OptionService, OptionCategoryService, OptionSyncService). Processors stay in place
for now — will be deleted in the cleanup stage.

OptionsController (Settings → Options):
- getList, get, create, update, delete
- bulkDelete (mass deletion)
- bulkAssign (assign options[] to categories[])
- getTypes (discover available option types + lexicon captions)
- getTree (MODX resource tree with checkbox state for option's categories)
- getModcategories (flat modCategory list for grouping dropdown)

CategoryOptionsController (Category → Options tab):
- getList (options linked to category with global caption/description)
- create, update (partial: value/active/required/position), delete
- sort (reorder positions atomically)
- bulk (activate/deactivate/require/unrequire/remove for many)
- duplicate (copy all links from another category, skip existing)

Per-link caption/description override is intentionally out of scope here
(lands after PR #203 which adds columns to ms3_category_options).
/api/mgr/options:
  GET    /types           list available option types
  GET    /tree            MODX resource tree (for category selection)
  GET    /modcategories   flat modCategory list (for group dropdown)
  POST   /bulk/assign     assign options[] to categories[]
  DELETE /bulk            mass delete
  GET    ''               list (query, modcategory_id, category_id, categories[])
  POST   ''               create
  GET    /{id}            detail + categories map
  PUT    /{id}            update
  DELETE /{id}            delete

/api/mgr/categories/{category_id}/options:
  POST   /sort            reorder positions
  POST   /bulk            activate/deactivate/require/unrequire/remove
  POST   /duplicate       copy from another category
  GET    ''               list for category
  POST   ''               add option to category
  PUT    /{option_id}     update link
  DELETE /{option_id}     remove link

Permissions: mssetting_save for options CRUD, mscategory_save for per-category links.
Static routes (/types, /tree, /bulk/...) precede /{id} to avoid path shadowing.
Replaces ms3-tree-option-categories + ms3-grid-option ExtJS combo with a single Vue app
mounted at #ms3-vue-options. Uses the REST endpoints introduced earlier in this branch.

New components:
- OptionCategoryTree.vue — MODX resource tree with PrimeVue Tree (checkbox selection with
  hierarchical partial-check, lazy loading, filter, context menu: refresh, expand/collapse
  branch, select all/clear all in subtree).
- OptionValuesEditor.vue — inline editor for option.properties.values, two variants:
  'simple' (plain list with drag-drop via vuedraggable) for combobox/comboMultiple,
  'colors' (label+hex pairs with color swatch) for comboColors.
- OptionsGrid.vue — main screen: category tree (left) + DataTable (right) + toolbar
  (create, modcategory filter, search, bulk assign, bulk delete) + Dialog for create/edit
  with OptionCategoryTree inside for link assignment + bulk-assign Dialog.

Wiring:
- vite.config.js — new 'options' entry
- entries/options.js — mount point with standard tab-visibility observer pattern
- settings.panel.js — replaces the two ExtJS xtypes with a single #ms3-vue-options div
- settings.class.php — drops option/grid.js, option/window.js, option/tree.js and per-type
  JS scripts from the page bundle; adds options.min.js/css

Legacy ExtJS option files (option/grid.js, option/window.js, option/tree.js, option/types/*)
are no longer loaded. They will be deleted in the final cleanup commit of this branch.
…ptionsTab

Adds CategoryOptionsTab.vue for the category-edit page (msCategory form, Options group).
Uses the REST /api/mgr/categories/{id}/options endpoints introduced earlier.

Feature parity with legacy ExtJS ms3-grid-category-option + ms3-window-option-add +
ms3-window-copy-category:
- DataTable with key, caption, type, value (inline cell edit), active/required indicators
- Row reorder via PrimeVue rowReorder (drag handle column) → POST /sort with option_ids
- Bulk actions toolbar on selection: activate / deactivate / require / unrequire / remove
  (mass confirm for remove)
- Single-row remove action with confirm
- Add option Dialog: Select of options not yet linked to this category, default value,
  active/required checkboxes
- Copy from category Dialog: Select of other categories (flat list from tree endpoint),
  backend handles skip-duplicates via /duplicate endpoint

Wiring:
- vite.config.js — new 'category-options' entry
- entries/category-options.js — mounts at #ms3-vue-category-options reading
  data-category-id from the container
- category/update.js addOptions() — replaces ms3-grid-category-option xtype with a
  <div id="ms3-vue-category-options" data-category-id="{id}"> container
- category/update.class.php — drops option.grid.js, option.windows.js; adds
  category-options Vue bundle

Legacy ExtJS category option files will be deleted in the final cleanup commit.
Delete unused ExtJS files and legacy MODX processors that are fully replaced by the
Vue UI + REST controllers in this branch:

Assets:
- assets/.../js/mgr/settings/option/{grid,window,tree}.js — replaced by OptionsGrid.vue
- assets/.../js/mgr/settings/option/types/{combobox,combobox-colors}.grid.js —
  replaced by OptionValuesEditor.vue (variant='simple' | 'colors')
- assets/.../js/mgr/category/option.{grid,windows}.js — replaced by CategoryOptionsTab.vue
- assets/.../css/mgr/main.css — drop selectors for deleted #ms3-window-option-* trees

Processors (all functionality moved to OptionsController + CategoryOptionsController):
- src/Processors/Settings/Option/*.php (11 files)
- src/Processors/Category/Option/*.php (11 files)

Kept intentionally:
- src/Controllers/Options/Types/*.php + msOption::getManagerField() — getSchema() and
  getValue()/getRowValue() are still used by OptionLoaderService. getField() (ExtJS
  string form) stays for backward-compat in CategoryOptionService::getOptionsForCategory
  until that service is retired in a follow-up.
- assets/.../js/mgr/misc/ms3.combo.js — still hosts shared combos (User, Customer,
  Category, etc.) used outside options.
- assets/.../js/mgr/misc/ms3.utils.js — utility grab-bag still used across the codebase.
getTree() now mirrors legacy Processors\Category\GetNodes behavior:
- WHERE modResource.class_key LIKE '%msCategory' AND deleted = 0 — excludes regular
  pages, sections, trash. User reported regular resources appearing in the picker.
- leaf flag derived from a LEFT JOIN childrenCount (vs. the isfolder heuristic) — only
  truly childless msCategory nodes render without an expand caret. User reported caret
  appearing only on the first node because isfolder doesn't reflect current child state
  for a just-emptied category.
PrimeVue Tree with selection-mode=checkbox defaults to hierarchical propagation:
checking a parent selects all descendants; unchecking any child unchecks the parent.

This prevents two legitimate cases pointed out by the user:
  1. Assign an option only to the parent category (without children).
  2. Assign one set of options to a parent and a different set to its children.

Disable propagation via :propagate-selection-up/:propagate-selection-down = false
so each node is independently checkable (matches legacy ExtJS tree behavior).
…tion

The earlier attempt with :propagate-selection-up/down props had no effect: in
PrimeVue 4.3.1, the Tree's checkbox mode hard-codes parent↔child propagation
in its private propagateDown/propagateUp methods with no escape hatch.

Drop selection-mode=checkbox entirely and render a plain <Checkbox> inside
the node slot, backed by a Set<categoryId> state we own. Ticking a parent no
longer cascades; unticking a child no longer resets the parent.

bulkToggleChecks (context-menu 'select all' / 'clear all' over a subtree) still
does recursive fill/clear, but as an explicit user action through the menu,
not as implicit checkbox coupling.
Second search field (in the grid toolbar) was redundant and confusing —
the tree filter on the left already searches categories, and filtering
options by key/caption can be done via the modcategory dropdown or by
picking specific categories in the tree. The grid-level search was also
non-live (only fired on Enter), which added to the confusion.

Leaves a cleaner toolbar: Create / modcategory filter / bulk actions.
…ee built-in

OptionCategoryTree had two filter inputs side-by-side:
- a custom IconField wrapper wiring filterValue into Tree's :filter-value
- PrimeVue Tree's own built-in filter input (auto-rendered by :filter=true)

Remove the custom one (and the filterValue ref, IconField/InputIcon/InputText imports)
and use Tree's native :filter-placeholder so the single built-in input shows the
localized 'Поиск' label. Same UX, one input instead of two.
…f mount root

Dialog markup is teleported to <body> by default, so selectors rooted at
.vueApp .options-grid-app don't match the dialog contents. Two fixes:

- Put a 'vueApp' class on each Dialog (class='ms3-option-dialog vueApp') so
  the child components (OptionCategoryTree, OptionValuesEditor) whose styles
  are scoped under .vueApp start working inside the dialog again.

- Move dialog-specific styles (.dialog-layout, .dialog-form, .dialog-tree,
  .form-row, .form-field, .dialog-hint) out from under .options-grid-app and
  scope them by .ms3-option-dialog instead.

Also flip .form-row to a column flex by default with a .two-cols modifier
for side-by-side fields — restores the comfortable spacing between labels
and inputs that got collapsed when all rows were rendered as row-flex.
ms3_options.modcategory_id is NOT NULL in the schema (default 0), so the
Vue dialog clearing the 'Group' Select sent null and the save failed.

Coerce null/'' to 0 for modcategory_id in applyWritableFields — other fields
stay as-is. Users can now leave Group empty when creating an option.
…boColors editor

Clicking the color swatch now opens a native PrimeVue ColorPicker palette
(matching the prior ExtJS behavior). The adjacent hex input is kept for
manual typing — both stay in sync and share the same properties.values entry.

Storage contract unchanged: option.properties.values[i].name keeps the
leading '#' (e.g. '#FF0000'). ColorPicker uses hex without '#' internally,
so hexWithoutHash/hexWithHash adapt between the two formats on each update.
For comboMultiple, comboColors and comboOptions on the product form, the Vue
renderer used to emit one hidden <input name='options-{key}[]'> per selected value.
ExtJS BasicForm.getValues() on save would then find multiple DOM nodes sharing the
same name and silently keep only the last one — user reported this as 'saves only
one of the selected values'.

Fix:
- ProductOptionField.vue renders a single <input name='options-{key}'> whose value
  is JSON.stringify(multiArrayValue) for all three multi-pick types.
- Utils::decodeOptionValue() helper parses that JSON array back into a PHP array
  on POST (leaves scalars and native arrays untouched).
- Product\Create::beforeSet and Product\Update::beforeSet call
  decodeOptionValue() alongside Utils::extractOptionKey so OptionSyncService
  receives an array for multi-value keys, as prepareOptionValues() already expects.
convertPreloadedValue() treated only 'ComboMultiple' (strict CamelCase string
match) as multi-value; msOption.type is actually stored lowerCamelCase so the
branch never ran and the function returned values[0] — dropping all but the
first pick when the product form was reloaded.

User reported 'сохранение не подтверждается' after saving multi-value
options. DB persistence via syncMultipleValues was fine all along; it was the
read path on the next page load that truncated the array.

Fix: case-insensitive match against [comboMultiple, comboColors, comboOptions]
so all three multi-pick types reload with the full value set.

Also drop the debug logging that was added to diagnose the issue.
The comboOptions type (List with autocomplete) must allow users to type ANY
value on the product form and get suggestions built from values already saved
by other products for the same option key. The MultiSelect used earlier only
permitted picking from the already-selected set, so typing new values was
impossible — user reported it was unusable.

Changes:
- ProductOptionField.vue: switch comboOptions from MultiSelect to PrimeVue
  AutoComplete (multiple mode, typeahead=false). Freeform text is accepted
  on Enter; suggestions list is populated on @complete via a small request.
  Hidden input still ships JSON.stringify(value) for the existing server path.
- OptionsController::getSuggestions() — new GET /api/mgr/options/suggestions
  endpoint. Returns DISTINCT msProductOption.value rows for a given key,
  optionally filtered by ?query= substring. Used as the autocomplete source.
Removing :typeahead=false (which suppresses the dropdown AND the @complete
event) and setting :min-length=1 with :delay=200 so the suggestions request
fires as the user types, matching standard PrimeVue AutoComplete behavior.
PrimeVue AutoComplete multiple mode only commits a suggestion token on Enter
when the typed text matches an item from the suggestions list. For the
free-form comboOptions type the user needs to be able to add anything —
including words that have never been saved before — so an @keydown handler
picks up Enter on unmatched input, pushes the text into v-model and clears
the input. Selecting from the suggestion list keeps working as before.
…-form input

The AutoComplete multiple mode kept swallowing Enter without pushing the typed
value into v-model, even with a custom keydown handler — user kept saving an
empty value.

Chips is the straightforward fit for this case: typing + Enter (or comma) adds
a new token, no suggestions panel required. Suggestions endpoint stays in the
backend for possible future re-use but we no longer fetch them on the product
form. Storage contract unchanged: hidden input still carries JSON.stringify(values)
and the Product processors decode via Utils::decodeOptionValue().
Switching from deprecated Chips to InputChips (PrimeVue v4 name), plus :add-on-blur=true
so typing a value and clicking outside the field automatically adds it as a token —
the previous Enter/comma-only behavior was non-obvious, user kept losing typed values
by moving focus away.

Placeholder updated to spell out the three ways to commit a chip (Enter, comma, blur).
Release notes for the full Vue migration of product options management.
…e into Vue refactor

PR #203 (#200) adds per-link caption/description override on msCategoryOption:
non-empty override wins over msOption.caption/description, empty inherits. That PR
predates the ExtJS→Vue options refactor in this branch and targets processors/ExtJS
that no longer exist here, so we cherry-pick the schema + service + overlay changes
and re-implement the UI portion on top of the new REST + Vue stack.

Backend (from PR #203, applied as-is):
- Migration 20260417120000_add_caption_description_to_category_options.php —
  adds ms3_category_options.caption (varchar 191, null) and .description (text, null).
- schema/minishop3.mysql.schema.xml + src/Model/msCategoryOption.php +
  src/Model/mysql/msCategoryOption.php — new fields in xPDO meta.
- OptionCategoryService::addToCategory() and OptionService::addOptionToCategory()
  accept optional caption/description override.
- OptionLoaderService — caption/description overlay for loadForProduct,
  getFieldsForProduct, loadForProducts (batched, no N+1 per product). Non-empty
  override wins; tie-breaking across multiple product categories: parent category
  first, then lowest position, then lowest category_id.
- ru/en lexicons: ms3_category_option_caption_override, *_description_override,
  ms3_global_caption, ms3_effective_caption.

Frontend (new work on top of PR #203):
- CategoryOptionsController formats rows with global_caption / global_description /
  category_caption / category_description and the effective caption; list search now
  also matches the per-link override columns. create() accepts caption/description;
  update() allows partial edit of both (empty string → null, so clearing the field
  restores inheritance).
- CategoryOptionsTab.vue — new columns 'Глобально' (global_caption, read-only) and
  'Название (для категории)' (category_caption, inline-editable). Inline cell save
  maps category_caption → caption on the wire. Add-option dialog gained caption +
  description override inputs.

Kept the case-insensitive convertPreloadedValue fix from earlier in this branch
(multiTypes includes comboMultiple, comboColors, comboOptions).
…203 cherry-pick

Pulling lexicon files from the PR #203 branch overwrote the ms3_option_create,
ms3_categories, ms3_modcategory_filter, ms3_options_assign_hint, etc. keys that
were added earlier in this branch for the new Vue UI. Merge both sets together.
…ption on hydration

After PR #203 added caption/description columns to ms3_category_options, the
getFieldsForProduct() query selects those columns alongside msOption.caption /
description. xPDO hydration maps columns by name — the second caption/description
overwrote the global one, so every option came out with an empty label (user saw
option keys instead of captions).

Exclude caption/description from the msCategoryOption select. The per-category
override is already layered on top by the overlay logic below — that path still
queries msCategoryOption directly for its values.
- OR-group wrapping in CategoryOptionsController::getList — previously the OR: search
  conditions were appended to the same array as category_id, making category_id match
  an OR term instead of being ANDed. Now wrapped in a nested array for proper grouping.
- Exact class_key match in OptionsController::getTree (modResource.class_key =
  'MiniShop3\Model\msCategory' instead of LIKE '%msCategory') — avoids accidental
  matches on third-party classes with a msCategory-suffixed name.
- i18n for product option form: booleanOptions label uses lexicon yes/no;
  InputChips placeholder for comboOptions uses new ms3_combo_options_chips_placeholder
  key with ru/en translations.
@biz87
Copy link
Copy Markdown
Member Author

biz87 commented Apr 20, 2026

Review фикс:

  • п.1 — CategoryOptionsController::getList — OR-условия теперь в nested-массиве (группировка AND category_id с OR-поиском)
  • п.2 — OptionsController::getTree — точный match class_key = MiniShop3\Model\msCategory вместо LIKE
  • п.5-6 — booleanOptions и InputChips placeholder через лексиконы (ms3_combo_options_chips_placeholder + ru/en)

Остальные пункты (3, 4, 7, 8, 9, 10, 11, 12) — оставляем как есть: либо осознанный trade-off, либо nit.

Коммит: 342c996

#1 TZ shift: DatePicker value is a local Date; toISOString() returned UTC
YYYY-MM-DD, which is a day earlier for TZ east of UTC. Introduce
formatDateForPost() that uses getFullYear/getMonth/getDate.

#4 Class literal: replace the string 'MiniShop3\Model\msCategory' in
OptionsController::getTree() with msCategory::class — IDE-refactor safe and
consistent with modResource::class nearby.

#5 Formatter consistency: CategoryOptionsController::formatRow() now delegates
caption merging to OptionLoaderService::mergeCaptionDescription() — same trim
semantics as the storefront overlay, so a ' ' override no longer shows in the
admin grid while the site still falls back to global.
@biz87
Copy link
Copy Markdown
Member Author

biz87 commented Apr 20, 2026

Round 2 фикс:

По #2 suggestions: оставляю InputChips (UX согласован с автором — AutoComplete в multi-freeform ломался ранее). Endpoint /api/mgr/options/suggestions оставлен публичным для custom-расширений — уберу упоминание из описания PR.

Остальные пункты (#3, #6#10) — осознанные trade-off / nit, по ним отдельные issue при необходимости.

Коммит: c309411

The 'autocomplete' part of comboOptions was missing — user pointed out the type name
promises suggestions but the field rendered pure InputChips. Keep InputChips for
reliable free-form entry (the earlier attempt with AutoComplete multiple mode was
unreliable with Enter on unmatched text) and add a suggestions strip below the field.

- Suggestions pulled lazily on the first keystroke from /api/mgr/options/suggestions
  (distinct msProductOption.value rows for the current key from all other products).
- Clicking a pill adds it to the chips and removes it from the strip.
- The strip filters as the user types and hides already-picked values.
- Two new lexicon keys: ms3_combo_options_suggestions (ru/en).
@Ibochkarev
Copy link
Copy Markdown
Member

Code review (архитектура, чистый код, продакшн)

Спасибо за масштабный и хорошо задокументированный PR. Ниже — сжатый ревью по изменениям и обсуждению в #205.

Что сделано сильно

  • Границы ответственности: логика вынесена в существующие сервисы (OptionService, OptionCategoryService, OptionSyncService, OptionLoaderService), контроллеры выступают фасадом к REST — это соответствует SRP и упрощает тестирование и сопровождение.
  • Декларативная схема (msOptionType::getSchema()) вместо генерации ExtJS-строк — понятный контракт для Vue и путь к постепенному избавлению от legacy getField().
  • Регрессии закрыты осознанно: JSON в одном hidden input для multi-value + decodeOptionValue() — прагматичный обход поведения BasicForm.getValues(); case-insensitive / единый набор multi-типов в convertPreloadedValue — исправляет реальный баг reload.
  • Итерации по ревью (коммиты вроде 342c996, c309411): группировка условий поиска в getList, точный class_key / msCategory::class, разбор коллизии гидратации xPDO с caption/description, TZ-safe дата без toISOString(), выравнивание formatRow с mergeCaptionDescription — это именно тот уровень внимания к краевым случаям, который нужен в продакшене.
  • UX/PrimeVue: отказ от встроенного checkbox-дерева в пользу собственного Set + чекбоксы там, где фреймворк навязывал каскад — правильный trade-off при несовпадении модели с продуктовыми требованиями.
  • Документация и релиз: CHANGELOG, версия, лексиконы, чеклист тест-плана — для такого объёма изменений это критично и выполнено.

Замечания и риски (без блокера к мержу, если вы уже их приняли)

  1. Utils::decodeOptionValue() — стоит убедиться, что на вход допускаются только ожидаемые форматы (валидный JSON-массив нужной глубины / типов), чтобы снизить риск неожиданных структур при ручном POST; если там уже есть жёсткая нормализация — ок.
  2. Публичный endpoint suggestions — раз он остаётся для расширений, имеет смысл в описании API/доке кратко зафиксировать контракт (query-параметры, лимиты, что отдаётся), чтобы не ломать сторонние интеграции при внутренних правках.
  3. Размер Vue-компонентов (OptionsGrid, CategoryOptionsTab, ProductOptionField — сотни строк): функционально оправдано для первой итерации; для долгой поддержки можно позже вынести подкомпоненты (диалоги, тулбар, колонки) без смены поведения.

Clean Code (краткий чеклист по PR)

  • Имена и разделение слоёв читаются последовательно; дублирование бизнес-правил caption/description сведено к общему merge в сервисе — хороший шаг к единому источнику правды.
  • Комментарии в описании PR и в коммитах объясняют «почему», а не только «что» — для ревьюера это большой плюс.

Итог: с учётом уже внесённых правок по ревью и явных trade-off в треде PR выглядит готовым к мержу с точки зрения качества и сопровождаемости. Спасибо команде за аккуратную миграцию и прозрачную историю исправлений.

— Code review (чистый код / продакшн-фокус)

@biz87 biz87 merged commit db79290 into beta Apr 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] Переопределение названий опций при привязке к категории

3 participants