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
9 changes: 9 additions & 0 deletions core/components/minishop3/lexicon/en/vue.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,15 @@
$_lang['delete_field_confirm_message'] = 'Are you sure you want to delete field "{name}"?';
$_lang['field_deleted'] = 'Field deleted successfully';
$_lang['error_deleting_field'] = 'Error deleting field';
$_lang['field_editable'] = 'Editable field';
$_lang['editor_type'] = 'Editor type';
$_lang['editor_type_text'] = 'Text';
$_lang['editor_type_number'] = 'Number';
$_lang['editor_type_select'] = 'Dropdown';
$_lang['editor_options'] = 'Options for selection';
$_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.';

// Grid Fields Config - Add Field Dialog
$_lang['add_field'] = 'Add Field';
Expand Down
9 changes: 9 additions & 0 deletions core/components/minishop3/lexicon/ru/vue.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,15 @@
$_lang['delete_field_confirm_message'] = 'Вы уверены, что хотите удалить поле "{name}"?';
$_lang['field_deleted'] = 'Поле успешно удалено';
$_lang['error_deleting_field'] = 'Ошибка при удалении поля';
$_lang['field_editable'] = 'Редактируемое поле';
$_lang['editor_type'] = 'Тип редактора';
$_lang['editor_type_text'] = 'Текст';
$_lang['editor_type_number'] = 'Число';
$_lang['editor_type_select'] = 'Выпадающий список';
$_lang['editor_options'] = 'Опции для выбора';
$_lang['inline_edit_saved'] = 'Изменения сохранены';
$_lang['inline_edit_error'] = 'Ошибка сохранения';
$_lang['inline_edit_hint'] = 'Для быстрого редактирования в таблице товаров категории (двойной клик по ячейке) включите «Редактируемое поле» в колонке таблицы ниже или в диалоге редактирования колонки.';

// Customers Grid Widget
$_lang['customers_title'] = 'Клиенты';
Expand Down
4 changes: 3 additions & 1 deletion core/components/minishop3/src/Services/GridConfigService.php
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,9 @@ public function saveGridConfig(string $gridKey, array $fields): bool
// price type
'decimals', 'currency', 'currency_position', 'thousands_separator', 'decimal_separator',
// weight type
'unit', 'unit_position'
'unit', 'unit_position',
// inline edit (category-products). Add 'editor_options' when select editor is implemented in UI
'editable', 'editor_type',
];
foreach ($configKeys as $key) {
if (array_key_exists($key, $fieldData)) {
Expand Down
101 changes: 91 additions & 10 deletions core/components/minishop3/src/Services/Product/ProductDataService.php
Original file line number Diff line number Diff line change
Expand Up @@ -483,37 +483,118 @@ public function getProductData(int $productId): ?array
}

/**
* Update product data
* Allowed fields for inline / API update (msProductData)
*/
protected static array $allowedUpdateFields = [
'article', 'price', 'old_price', 'stock', 'weight',
'vendor_id', 'made_in', 'new', 'popular', 'favorite',
];

/**
* Resource (modResource) fields updatable via same API (e.g. published)
*/
protected static array $allowedResourceFields = ['published'];

/**
* Apply published state to product resource and save.
* Invokes OnDocPublished / OnDocUnPublished for plugin compatibility.
*
* @param msProduct $product
* @param int $published 0 or 1
* @return bool True if saved successfully
*/
protected function applyPublishedToResource(msProduct $product, int $published): bool
{
$product->set('published', $published);
if ($published) {
$product->set('publishedon', time());
$product->set('publishedby', $this->modx->user->get('id'));
} else {
$product->set('publishedon', 0);
$product->set('publishedby', 0);
}
if (!$product->save()) {
return false;
}
$eventName = $published ? 'OnDocPublished' : 'OnDocUnPublished';
$this->modx->invokeEvent($eventName, [
'id' => $product->get('id'),
'resource' => $product,
]);
return true;
}

/**
* Validate productData update values (minimal server-side validation).
*
* Loads product by ID, updates msProductData fields and saves
* Used in API controllers to update product data
* @param array $filtered Filtered allowed fields
* @return bool True if valid
*/
protected function validateProductDataUpdate(array $filtered): bool
{
if (isset($filtered['price']) && (float)$filtered['price'] < 0) {
return false;
}
if (isset($filtered['old_price']) && (float)$filtered['old_price'] < 0) {
return false;
}
if (isset($filtered['stock']) && (int)$filtered['stock'] != $filtered['stock']) {
return false;
}
if (isset($filtered['weight']) && (float)$filtered['weight'] < 0) {
return false;
}
return true;
}

/**
* Update product data (msProductData and optionally resource fields like published).
* Saves productData first, then resource (published) to avoid race and desync on failure.
*
* @param int $productId Product ID
* @param array $data Data to update
* @return array|null Updated data or null on error
*/
public function updateProductData(int $productId, array $data): ?array
{
if (!$this->modx->hasPermission('save_document')) {
return null;
}

/** @var msProduct $product */
$product = $this->modx->getObject(msProduct::class, $productId);

if (!$product) {
if (!$product || !$product->checkPolicy('save')) {
return null;
}

/** @var msProductData $productData */
$productData = $product->loadData();

if (!$productData) {
return null;
}

$productData->fromArray($data);
$filtered = array_intersect_key($data, array_flip(self::$allowedUpdateFields));
if (!$this->validateProductDataUpdate($filtered)) {
return null;
}

$productData->fromArray($filtered);
if (!$productData->save()) {
return null;
}

if ($productData->save()) {
return $productData->toArray();
$resourceData = array_intersect_key($data, array_flip(self::$allowedResourceFields));
if (isset($resourceData['published'])) {
$published = $resourceData['published'] ? 1 : 0;
if (!$this->applyPublishedToResource($product, $published)) {
return null;
}
}

return null;
$result = $productData->toArray();
if (isset($resourceData['published'])) {
$result['published'] = (bool)$product->get('published');
}
return $result;
}
}
Loading