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 core/components/minishop3/lexicon/en/default.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@
$_lang['ms3_err_ns'] = 'This field is required';
$_lang['ms3_err_ae'] = 'This field must be unique';
$_lang['ms3_err_json'] = 'This field requires JSON string';
$_lang['ms3_repeater_validation_error'] = 'Repeater field "[[+field]]": [[+error]]';

$_lang['ms3_err_user_nf'] = 'User not found.';
$_lang['ms3_err_order_nf'] = 'Order with this identifier not found.';
Expand Down
20 changes: 20 additions & 0 deletions core/components/minishop3/lexicon/en/vue.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@
$_lang['ms3_vue_xtype_textarea'] = 'Text Area';
$_lang['ms3_vue_xtype_xcheckbox'] = 'Checkbox';
$_lang['ms3_vue_xtype_combo_select'] = 'Dropdown List';
$_lang['ms3_vue_xtype_repeater'] = 'Repeater (rows grid)';
$_lang['ms3_vue_xtype_combo_vendor'] = 'Vendor (combo)';
$_lang['ms3_vue_xtype_combo_autocomplete'] = 'Autocomplete (combo)';
$_lang['ms3_vue_xtype_combo_options'] = 'Product Options (chips)';
Expand All @@ -169,6 +170,25 @@
$_lang['ms3_vue_select_options_placeholder'] = "value1==First option\nvalue2==Second option\nvalue3==Third option";
$_lang['ms3_vue_select_options_help'] = 'Format: value==label (one per line). If label is not specified, the value will be used.';

// Repeater field
$_lang['ms3_vue_repeater_schema_label'] = 'Repeater schema';
$_lang['ms3_vue_repeater_schema_help'] = 'Define columns for each row. Order is saved with automatic rank.';
$_lang['ms3_vue_repeater_columns'] = 'Columns';
$_lang['ms3_vue_repeater_add_column'] = 'Add column';
$_lang['ms3_vue_repeater_column_key'] = 'Key';
$_lang['ms3_vue_repeater_column_label'] = 'Label';
$_lang['ms3_vue_repeater_required'] = 'Required';
$_lang['ms3_vue_repeater_rank_field'] = 'Rank field';
$_lang['ms3_vue_repeater_min_rows'] = 'Min rows';
$_lang['ms3_vue_repeater_max_rows'] = 'Max rows';
$_lang['ms3_vue_repeater_unlimited'] = 'Unlimited';
$_lang['ms3_vue_repeater_sortable'] = 'Drag-and-drop sorting';
$_lang['ms3_vue_repeater_add_row'] = 'Add row';
$_lang['ms3_vue_repeater_no_columns'] = 'Configure repeater columns in extra field settings.';
$_lang['ms3_vue_repeater_drag_hint'] = 'Drag to reorder';
$_lang['ms3_vue_order_extra_fields'] = 'Additional order fields';
$_lang['ms3_vue_order_address_extra_fields'] = 'Additional address fields';

// Database types (dbtype)
$_lang['ms3_vue_dbtype_varchar'] = 'VARCHAR (string)';
$_lang['ms3_vue_dbtype_text'] = 'TEXT (text)';
Expand Down
1 change: 1 addition & 0 deletions core/components/minishop3/lexicon/ru/default.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@
$_lang['ms3_err_ns'] = 'Это поле обязательно';
$_lang['ms3_err_ae'] = 'Это поле должно быть уникально';
$_lang['ms3_err_json'] = 'Это поле требует JSON строку';
$_lang['ms3_repeater_validation_error'] = 'Поле повторителя «[[+field]]»: [[+error]]';

$_lang['ms3_err_user_nf'] = 'Пользователь не найден.';
$_lang['ms3_err_order_nf'] = 'Заказ с таким идентификатором не найден.';
Expand Down
20 changes: 20 additions & 0 deletions core/components/minishop3/lexicon/ru/vue.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@
$_lang['ms3_vue_xtype_textarea'] = 'Текстовая область';
$_lang['ms3_vue_xtype_xcheckbox'] = 'Флажок';
$_lang['ms3_vue_xtype_combo_select'] = 'Выпадающий список';
$_lang['ms3_vue_xtype_repeater'] = 'Повторитель (таблица строк)';
$_lang['ms3_vue_xtype_combo_vendor'] = 'Производитель (combo)';
$_lang['ms3_vue_xtype_combo_autocomplete'] = 'Автодополнение (combo)';
$_lang['ms3_vue_xtype_combo_options'] = 'Опции товара (chips)';
Expand All @@ -169,6 +170,25 @@
$_lang['ms3_vue_select_options_placeholder'] = "value1==Первый вариант\nvalue2==Второй вариант\nvalue3==Третий вариант";
$_lang['ms3_vue_select_options_help'] = 'Формат: значение==подпись (по одному на строку). Если подпись не указана, будет использовано значение.';

// Repeater field
$_lang['ms3_vue_repeater_schema_label'] = 'Схема повторителя';
$_lang['ms3_vue_repeater_schema_help'] = 'Определите колонки для каждой строки. Порядок сохраняется с автоматическим rank.';
$_lang['ms3_vue_repeater_columns'] = 'Колонки';
$_lang['ms3_vue_repeater_add_column'] = 'Добавить колонку';
$_lang['ms3_vue_repeater_column_key'] = 'Ключ';
$_lang['ms3_vue_repeater_column_label'] = 'Подпись';
$_lang['ms3_vue_repeater_required'] = 'Обязательное';
$_lang['ms3_vue_repeater_rank_field'] = 'Поле rank';
$_lang['ms3_vue_repeater_min_rows'] = 'Мин. строк';
$_lang['ms3_vue_repeater_max_rows'] = 'Макс. строк';
$_lang['ms3_vue_repeater_unlimited'] = 'Без ограничения';
$_lang['ms3_vue_repeater_sortable'] = 'Сортировка перетаскиванием';
$_lang['ms3_vue_repeater_add_row'] = 'Добавить строку';
$_lang['ms3_vue_repeater_no_columns'] = 'Настройте колонки повторителя в параметрах extra field.';
$_lang['ms3_vue_repeater_drag_hint'] = 'Перетащите для изменения порядка';
$_lang['ms3_vue_order_extra_fields'] = 'Дополнительные поля заказа';
$_lang['ms3_vue_order_address_extra_fields'] = 'Дополнительные поля адреса';

// Типы данных БД (dbtype)
$_lang['ms3_vue_dbtype_varchar'] = 'VARCHAR (строка)';
$_lang['ms3_vue_dbtype_text'] = 'TEXT (текст)';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

use Phinx\Migration\AbstractMigration;

/**
* Adds repeater_config column to ms3_extra_fields for ms3-repeater field schema
*/
final class AddRepeaterConfigToExtraFields extends AbstractMigration
{
public function up(): void
{
$table = $this->table('ms3_extra_fields');

if (!$table->hasColumn('repeater_config')) {
$table->addColumn('repeater_config', 'text', [
'null' => true,
'after' => 'select_options',
'comment' => 'JSON schema for ms3-repeater field type (columns, rankField, min/max rows)',
]);
$table->update();
}
}

public function down(): void
{
$table = $this->table('ms3_extra_fields');

if ($table->hasColumn('repeater_config')) {
$table->removeColumn('repeater_config');
$table->update();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,7 @@
<field key="attributes" dbtype="varchar" precision="191" phptype="string" null="true" default=""/>
<field key="index_type" dbtype="varchar" precision="50" phptype="string" null="true" default="NONE"/>
<field key="select_options" dbtype="text" phptype="string" null="true"/>
<field key="repeater_config" dbtype="text" phptype="string" null="true"/>

<field key="active" dbtype="tinyint" precision="1" phptype="boolean" null="true" default="0"/>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use MiniShop3\Model\msPayment;
use MiniShop3\Router\HttpStatus;
use MiniShop3\Router\Response;
use MiniShop3\Services\ExtraFields\RepeaterFieldService;
use MiniShop3\Services\CustomerDuplicateChecker;
use MiniShop3\Services\CustomerFactory;
use MiniShop3\Services\FilterConfigManager;
Expand Down Expand Up @@ -841,15 +842,29 @@ public function update(array $params = []): array

// Handle extra fields for msOrder (stored as real DB columns via Object Extension)
$orderExtraFields = $this->getExtraFieldKeys('MiniShop3\\Model\\msOrder');
foreach ($orderExtraFields as $extraField) {
if (array_key_exists($extraField, $params)) {
$oldValue = $order->get($extraField);
$newValue = $params[$extraField];
if ($oldValue != $newValue) {
$changedOrderFields[$extraField] = ['old' => $oldValue, 'new' => $newValue];
}
$order->set($extraField, $newValue);
foreach ($orderExtraFields as $extraFieldKey) {
if (!array_key_exists($extraFieldKey, $params)) {
continue;
}

$normalized = $this->normalizeExtraFieldValue(
'MiniShop3\\Model\\msOrder',
$extraFieldKey,
$params[$extraFieldKey]
);
if (!$normalized['ok']) {
return Response::error(
$normalized['message'],
HttpStatus::UNPROCESSABLE_ENTITY
)->getData();
}

$newValue = $normalized['value'];
$oldValue = $order->get($extraFieldKey);
if ($oldValue != $newValue) {
$changedOrderFields[$extraFieldKey] = ['old' => $oldValue, 'new' => $newValue];
}
$order->set($extraFieldKey, $newValue);
}

$order->set('updatedon', date('Y-m-d H:i:s'));
Expand Down Expand Up @@ -889,15 +904,29 @@ public function update(array $params = []): array

// Handle extra fields for msOrderAddress (stored as real DB columns via Object Extension)
$addressExtraFields = $this->getExtraFieldKeys('MiniShop3\\Model\\msOrderAddress');
foreach ($addressExtraFields as $extraField) {
if (array_key_exists($extraField, $params)) {
$oldValue = $address->get($extraField);
$newValue = $params[$extraField];
if ($oldValue != $newValue) {
$changedAddressFields[$extraField] = ['old' => $oldValue, 'new' => $newValue];
}
$address->set($extraField, $newValue);
foreach ($addressExtraFields as $extraFieldKey) {
if (!array_key_exists($extraFieldKey, $params)) {
continue;
}

$normalized = $this->normalizeExtraFieldValue(
'MiniShop3\\Model\\msOrderAddress',
$extraFieldKey,
$params[$extraFieldKey]
);
if (!$normalized['ok']) {
return Response::error(
$normalized['message'],
HttpStatus::UNPROCESSABLE_ENTITY
)->getData();
}

$newValue = $normalized['value'];
$oldValue = $address->get($extraFieldKey);
if ($oldValue != $newValue) {
$changedAddressFields[$extraFieldKey] = ['old' => $oldValue, 'new' => $newValue];
}
$address->set($extraFieldKey, $newValue);
}

$address->save();
Expand Down Expand Up @@ -1663,6 +1692,41 @@ protected function formatOrder(array $data): array
return $data;
}

/**
* @return array{ok: bool, value?: mixed, message?: string}
*/
protected function normalizeExtraFieldValue(string $modelClass, string $fieldKey, mixed $value): array
{
/** @var msExtraField|null $definition */
$definition = $this->modx->getObject(msExtraField::class, [
'class' => $modelClass,
'key' => $fieldKey,
'active' => true,
]);

if (!$definition || $definition->get('xtype') !== RepeaterFieldService::XTYPE) {
return ['ok' => true, 'value' => $value];
}

/** @var RepeaterFieldService $repeaterService */
$repeaterService = $this->modx->services->get('ms3_repeater_field');
$config = $repeaterService->parseConfig($definition->get('repeater_config'));

try {
return ['ok' => true, 'value' => $repeaterService->processValue($value, $config)];
} catch (\InvalidArgumentException $e) {
$this->modx->lexicon->load('minishop3:default');

return [
'ok' => false,
'message' => $this->modx->lexicon('ms3_repeater_validation_error', [
'field' => $fieldKey,
'error' => $e->getMessage(),
]),
];
}
}

/**
* Get extra field keys for a specific model class
*
Expand Down
8 changes: 7 additions & 1 deletion core/components/minishop3/src/Model/mysql/msExtraField.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class msExtraField extends \MiniShop3\Model\msExtraField
'index_type' => 'NONE',
'active' => 0,
'select_options' => null,
'repeater_config' => null,
],
'fieldMeta' => [
'class' => [
Expand Down Expand Up @@ -122,7 +123,12 @@ class msExtraField extends \MiniShop3\Model\msExtraField
'dbtype' => 'text',
'phptype' => 'string',
'null' => true,
]
],
'repeater_config' => [
'dbtype' => 'text',
'phptype' => 'string',
'null' => true,
],
],
'indexes' => [],
'composites' => [],
Expand Down
4 changes: 4 additions & 0 deletions core/components/minishop3/src/ServiceRegistry.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ class ServiceRegistry
'class' => \MiniShop3\Services\Product\ProductDataService::class,
'interface' => null,
],
'ms3_repeater_field' => [
'class' => \MiniShop3\Services\ExtraFields\RepeaterFieldService::class,
'interface' => null,
],
'ms3_product_image' => [
'class' => \MiniShop3\Services\Product\ProductImageService::class,
'interface' => null,
Expand Down
Loading