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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@

#### ♻️ Рефакторинг

- **Inline-edit select/combo в гриде товаров категории (#155, #157):** единый контракт опций `{ value, label }` на фронте; `GET references/vendors` дополняет ответ массивом `options` (поле `vendors` сохранено для совместимости); `GridEditorReferenceRegistry` и валидация combo при сохранении конфига; в конфиге колонок — `editor_reference` и опциональный allowlisted `editor_combo_endpoint`; `GridFieldsConfig` — выбор справочника и override URL; composable `useCategoryProductsInlineEdit` и утилиты `gridEditorOptions.js` вместо логики внутри `CategoryProductsGrid`
- **Экран заказа — provide/inject вместо props-цепочки (#196):** `provide(ORDER_CONTEXT_KEY)` в `OrderView`, composables `useOrderFormatters`, `useOrderFieldHelpers`, `useOrderLogFormatters`; вкладки получают только данные вкладки через props; безопасный `inject` до деструктуризации
- **OrderView разбит на подкомпоненты (#176):** монолитный `OrderView.vue` разделён на `OrderInfoTab`, `OrderProductsTab`, `OrderAddressTab`, `OrderHistoryTab` + вынесен `orderFieldsLayout.css`
- **Опции товара:** Map по `modcategory_id` для вкладок, именованный page size комбобокса под `ms3.grid`, документирован GROUP BY
Expand Down
15 changes: 15 additions & 0 deletions core/components/minishop3/lexicon/en/vue.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,21 @@
$_lang['editor_type'] = 'Editor type';
$_lang['editor_type_text'] = 'Text';
$_lang['editor_type_number'] = 'Number';
$_lang['editor_type_select'] = 'Select';
$_lang['editor_type_combo'] = 'Combo (API)';
$_lang['editor_options'] = 'Editor options';
$_lang['editor_options_hint'] = 'JSON array: [{ "label": "Russia", "value": "RU" }, ...]';
$_lang['editor_combo_endpoint'] = 'API endpoint for options';
$_lang['editor_combo_endpoint_placeholder'] = '/api/mgr/references/vendors';
$_lang['editor_combo_endpoint_required'] = 'API endpoint is required for combo editor type';
$_lang['editor_reference'] = 'Reference source';
$_lang['editor_reference_none'] = '— not selected —';
$_lang['editor_reference_hint'] = 'Built-in list (e.g. vendors). Optional if you use an allowlisted override URL below.';
$_lang['editor_combo_endpoint_override'] = 'Override URL (optional)';
$_lang['editor_combo_endpoint_override_hint'] = 'Only allowlisted paths (e.g. under /api/mgr/references/). If set, it overrides the reference URL when loading options.';
$_lang['editor_combo_ref_or_endpoint_required'] = 'For combo editor, choose a reference or enter an allowlisted API URL.';
$_lang['editor_combo_endpoint_not_allowlisted'] = 'Combo override URL is not on the allowlist.';
$_lang['combo_options_load_failed'] = 'Failed to load combo options';
$_lang['inline_edit_saved'] = 'Changes saved';
$_lang['inline_edit_error'] = 'Save error';
$_lang['inline_edit_hint'] = 'To enable inline editing in the category products table (double-click a cell), turn on «Editable field» in the column row below or in the column edit dialog.';
Expand Down
15 changes: 15 additions & 0 deletions core/components/minishop3/lexicon/ru/vue.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,21 @@
$_lang['editor_type'] = 'Тип редактора';
$_lang['editor_type_text'] = 'Текст';
$_lang['editor_type_number'] = 'Число';
$_lang['editor_type_select'] = 'Выпадающий список';
$_lang['editor_type_combo'] = 'Комбо (API)';
$_lang['editor_options'] = 'Опции редактора';
$_lang['editor_options_hint'] = 'JSON-массив: [{ "label": "Россия", "value": "RU" }, ...]';
$_lang['editor_combo_endpoint'] = 'API endpoint для опций';
$_lang['editor_combo_endpoint_placeholder'] = '/api/mgr/references/vendors';
$_lang['editor_combo_endpoint_required'] = 'Для типа редактора «Комбо» обязателен API endpoint';
$_lang['editor_reference'] = 'Источник справочника';
$_lang['editor_reference_none'] = '— не выбрано —';
$_lang['editor_reference_hint'] = 'Встроенный список (например vendors). Необязателен, если ниже указан допустимый URL переопределения.';
$_lang['editor_combo_endpoint_override'] = 'URL переопределения (необязательно)';
$_lang['editor_combo_endpoint_override_hint'] = 'Только пути из белого списка (например под /api/mgr/references/). Если задан, при загрузке опций заменяет URL из справочника.';
$_lang['editor_combo_ref_or_endpoint_required'] = 'Для комбо выберите справочник или укажите допустимый API URL.';
$_lang['editor_combo_endpoint_not_allowlisted'] = 'URL переопределения комбо не входит в белый список.';
$_lang['combo_options_load_failed'] = 'Не удалось загрузить опции комбо';
$_lang['inline_edit_saved'] = 'Изменения сохранены';
$_lang['inline_edit_error'] = 'Ошибка сохранения';
$_lang['inline_edit_hint'] = 'Для быстрого редактирования в таблице товаров категории (двойной клик по ячейке) включите «Редактируемое поле» в колонке таблицы ниже или в диалоге редактирования колонки.';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

namespace MiniShop3\Controllers\Api\Manager;

use MiniShop3\Services\GridConfigService;
use MiniShop3\Router\Response;
use MiniShop3\Services\GridConfigService;
use MiniShop3\Services\GridEditorReferenceRegistry;
use MODX\Revolution\modX;

/**
Expand Down Expand Up @@ -48,7 +49,12 @@ public function getConfig(array $params): array
$includeHidden = !empty($params['include_hidden']);
$config = $this->service->getGridConfig($gridKey, $includeHidden);

return Response::success(['columns' => $config])->getData();
$payload = ['columns' => $config];
if ($gridKey === 'category-products') {
$payload['editor_references'] = GridEditorReferenceRegistry::listForClient();
}

return Response::success($payload)->getData();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,18 @@ public function getVendors(array $params): Response
];
}

$options = [];
foreach ($vendors as $row) {
$options[] = [
'value' => $row['id'],
'label' => (string)$row['name'],
];
}

return Response::success([
'vendors' => $vendors,
'total' => count($vendors)
'options' => $options,
'total' => count($vendors),
]);
} catch (\Exception $e) {
$this->modx->log(\MODX\Revolution\modX::LOG_LEVEL_ERROR, '[ReferencesController] ' . $e->getMessage());
Expand Down
19 changes: 19 additions & 0 deletions core/components/minishop3/src/Services/GridColumnEditorType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace MiniShop3\Services;

/**
* Allowed values for msGridField JSON config key `editor_type` (category-products inline edit).
*/
final class GridColumnEditorType
{
public const TEXT = 'text';

public const NUMBER = 'number';

public const SELECT = 'select';

public const COMBO = 'combo';
}
34 changes: 32 additions & 2 deletions core/components/minishop3/src/Services/GridConfigService.php
Original file line number Diff line number Diff line change
Expand Up @@ -185,15 +185,27 @@ public function saveGridConfig(string $gridKey, array $fields): bool
'decimals', 'currency', 'currency_position', 'thousands_separator', 'decimal_separator',
// weight type
'unit', 'unit_position',
// inline edit (category-products). Add 'editor_options' when select editor is implemented in UI
'editable', 'editor_type',
// inline edit (category-products)
'editable', 'editor_type', 'editor_options', 'editor_reference', 'editor_combo_endpoint',
];
foreach ($configKeys as $key) {
if (array_key_exists($key, $fieldData)) {
$config[$key] = $fieldData[$key];
}
}

if (($config['editor_type'] ?? '') !== GridColumnEditorType::COMBO) {
unset($config['editor_reference'], $config['editor_combo_endpoint']);
}

$comboCheck = GridEditorReferenceRegistry::validateComboEditorConfig($config);
if (!$comboCheck['success']) {
$this->modx->log(modX::LOG_LEVEL_ERROR,
'[GridConfigService] Combo editor validation failed for ' . $gridKey . '.' . $fieldName . ': ' . ($comboCheck['message'] ?? ''));

return false;
}

$field->set('config', json_encode($config, JSON_UNESCAPED_UNICODE));

if (!$field->save()) {
Expand Down Expand Up @@ -354,6 +366,15 @@ public function addField(string $gridKey, array $data): array
// Add type to config
$config['type'] = $type;

if (($config['editor_type'] ?? '') !== GridColumnEditorType::COMBO) {
unset($config['editor_reference'], $config['editor_combo_endpoint']);
}

$comboCheck = GridEditorReferenceRegistry::validateComboEditorConfig($config);
if (!$comboCheck['success']) {
return ['success' => false, 'message' => $comboCheck['message'] ?? 'Invalid combo editor configuration'];
}

// Get maximum sort_order
$maxSortOrder = 0;
$query = $this->modx->newQuery(msGridField::class);
Expand Down Expand Up @@ -471,6 +492,15 @@ public function updateField(string $gridKey, string $fieldName, array $data): ar
// Add type to config
$config['type'] = $type;

if (($config['editor_type'] ?? '') !== GridColumnEditorType::COMBO) {
unset($config['editor_reference'], $config['editor_combo_endpoint']);
}

$comboCheck = GridEditorReferenceRegistry::validateComboEditorConfig($config);
if (!$comboCheck['success']) {
return ['success' => false, 'message' => $comboCheck['message'] ?? 'Invalid combo editor configuration'];
}

// Update field
if (isset($data['label'])) {
$field->set('label', $data['label']);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<?php

namespace MiniShop3\Services;

/**
* Whitelist of grid inline-edit combo references (key → manager API path).
* Single place for validation, resolution, and client metadata.
*/
final class GridEditorReferenceRegistry
{
/** @var array<string, string> */
private const REFERENCES = [
'vendors' => '/api/mgr/references/vendors',
];

private const ALLOWED_PATH_PREFIX = '/api/mgr/references/';

/**
* @return list<array{key: string, path: string}>
*/
public static function listForClient(): array
{
$out = [];
foreach (self::REFERENCES as $key => $path) {
$out[] = ['key' => $key, 'path' => $path];
}

return $out;
}

public static function pathFor(string $referenceKey): ?string
{
$k = trim($referenceKey);

return self::REFERENCES[$k] ?? null;
}

public static function isAllowlistedEndpoint(string $url): bool
{
$trim = trim($url);
if ($trim === '') {
return false;
}
if ($trim[0] !== '/') {
return false;
}
$path = parse_url($trim, PHP_URL_PATH);
if (!is_string($path) || $path === '') {
$path = $trim;
}

return str_starts_with($path, self::ALLOWED_PATH_PREFIX);
}

/**
* Validate combo editor configuration before persisting.
*
* @param array<string, mixed> $config Field JSON config (may include editor_*)
* @return array{success: bool, message?: string}
*/
public static function validateComboEditorConfig(array $config): array
{
if (($config['editor_type'] ?? '') !== GridColumnEditorType::COMBO) {
return ['success' => true];
}

$ref = trim((string)($config['editor_reference'] ?? ''));
$endpoint = trim((string)($config['editor_combo_endpoint'] ?? ''));

$refPath = $ref !== '' ? self::pathFor($ref) : null;
if ($ref !== '' && $refPath === null) {
return ['success' => false, 'message' => 'Unknown editor_reference'];
}
if ($endpoint !== '' && !self::isAllowlistedEndpoint($endpoint)) {
return [
'success' => false,
'message' => 'editor_combo_endpoint must start with path ' . self::ALLOWED_PATH_PREFIX,
];
}
if ($refPath === null && $endpoint === '') {
return [
'success' => false,
'message' => 'Combo editor requires a known editor_reference or an allowlisted editor_combo_endpoint',
];
}

return ['success' => true];
}
}
Loading