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
8 changes: 8 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,14 @@
$_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['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
8 changes: 8 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,14 @@
$_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['combo_options_load_failed'] = 'Не удалось загрузить опции комбо';
$_lang['inline_edit_saved'] = 'Изменения сохранены';
$_lang['inline_edit_error'] = 'Ошибка сохранения';
$_lang['inline_edit_hint'] = 'Для быстрого редактирования в таблице товаров категории (двойной клик по ячейке) включите «Редактируемое поле» в колонке таблицы ниже или в диалоге редактирования колонки.';
Expand Down
4 changes: 2 additions & 2 deletions core/components/minishop3/src/Services/GridConfigService.php
Original file line number Diff line number Diff line change
Expand Up @@ -185,8 +185,8 @@ 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_combo_endpoint',
];
foreach ($configKeys as $key) {
if (array_key_exists($key, $fieldData)) {
Expand Down
96 changes: 91 additions & 5 deletions vueManager/src/components/CategoryProductsGrid.vue
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,10 @@ const editingCell = ref(null)
const inlineEditValue = ref('')
/** True while inline edit save request is in progress */
const inlineEditSaving = ref(false)
/** Ref to the current inline-edit input (one of Checkbox/InputText/InputNumber) for focus */
/** Ref to the current inline-edit input (one of Checkbox/InputText/InputNumber/Select) for focus */
const inlineEditInputRef = ref(null)
/** Options for combo editor (loaded from API when cell is opened) */
const comboOptionsRef = ref([])

// Default thumbnail from system settings

Expand Down Expand Up @@ -367,23 +369,26 @@ function isEditingCell(product, column) {
/**
* Start inline edit on double-click.
* Blocks if another cell is currently saving to avoid race condition.
* For combo editor, loads options from API before focusing.
*/
function startInlineEdit(product, column) {
async function startInlineEdit(product, column) {
if (!column.editable) return
if (inlineEditSaving.value) return
editingCell.value = { productId: product.id, columnName: column.name }
const raw = product[column.name]
inlineEditValue.value = raw === null || raw === undefined ? '' : raw
// autofocus doesn't work on dynamically inserted elements; focus via ref after DOM update
if ((column.editor_type || 'text') === 'combo') {
await loadComboOptions(column)
}
nextTick(() => {
const comp = inlineEditInputRef.value
if (!comp) return
const el = comp.$el?.querySelector?.('input') ?? comp.$el ?? comp
const el = comp.$el?.querySelector?.('input') ?? comp.$el?.querySelector?.('.p-dropdown') ?? comp.$el ?? comp
if (el?.focus) el.focus()
})
}

/** Boolean columns (e.g. published) use type, not editor_type (select not in UI yet) */
/** Boolean columns (e.g. published) use type, not editor_type */
function isBooleanColumn(column) {
return column.type === 'boolean'
}
Expand All @@ -396,6 +401,9 @@ function normalizeValueForSave(rawValue, column) {
const num = Number(rawValue)
return Number.isNaN(num) ? null : num
}
if (editorType === 'select' || editorType === 'combo') {
return rawValue
}
return rawValue
}

Expand All @@ -409,6 +417,11 @@ function isInlineValueUnchanged(original, value, column) {
v === null || v === undefined || v === '' || Number.isNaN(Number(v)) ? null : Number(v)
return norm(original) === norm(value)
}
if (editorType === 'select' || editorType === 'combo') {
const o = original === null || original === undefined ? null : original
const v = value === null || value === undefined ? null : value
return o === v || String(o) === String(v)
}
const origStr = original === null || original === undefined ? '' : String(original)
const valStr = value === null || value === undefined ? '' : String(value)
return origStr === valStr
Expand All @@ -417,6 +430,44 @@ function isInlineValueUnchanged(original, value, column) {
function clearInlineEditState() {
editingCell.value = null
inlineEditValue.value = ''
comboOptionsRef.value = []
}

/**
* Normalize combo API response to [{ id, label }]. Supports vendors: [{id, name}], options: [{id, label}], data: [{id, name}].
*/
function normalizeComboResponse(response) {
if (!response || typeof response !== 'object') return []
const list = response.vendors ?? response.options ?? response.data ?? []
if (!Array.isArray(list)) return []
return list.map((item) => ({
id: item.id ?? item.value,
label: item.name ?? item.label ?? String(item.id ?? item.value ?? ''),
}))
}

/**
* Load combo options from column's editor_combo_endpoint
*/
async function loadComboOptions(column) {
const endpoint = column.editor_combo_endpoint
if (!endpoint || typeof endpoint !== 'string') {
comboOptionsRef.value = []
return
}
try {
const response = await request.get(endpoint)
comboOptionsRef.value = normalizeComboResponse(response)
} catch (err) {
console.error('[CategoryProductsGrid] Combo options load failed:', err)
comboOptionsRef.value = []
toast.add({
severity: 'warn',
summary: _('error'),
detail: _('combo_options_load_failed') || err.message,
life: 3000,
})
}
}

/**
Expand Down Expand Up @@ -1060,6 +1111,41 @@ onMounted(async () => {
@keydown.enter.prevent="$event.target.blur()"
@keydown.escape="cancelInlineEdit"
/>
<div
v-else-if="(column.editor_type || 'text') === 'select'"
class="inline-edit-input-wrapper w-full"
@keydown.enter.capture.prevent="$event.target?.blur?.()"
@keydown.escape.capture.prevent="cancelInlineEdit"
>
<Select
ref="inlineEditInputRef"
v-model="inlineEditValue"
:options="column.editor_options || []"
option-label="label"
option-value="value"
class="w-full"
:disabled="inlineEditSaving"
@change="saveInlineEdit(product, column)"
/>
</div>
<div
v-else-if="(column.editor_type || 'text') === 'combo'"
class="inline-edit-input-wrapper w-full"
@keydown.enter.capture.prevent="$event.target?.blur?.()"
@keydown.escape.capture.prevent="cancelInlineEdit"
>
<Select
ref="inlineEditInputRef"
v-model="inlineEditValue"
:options="comboOptionsRef"
option-label="label"
option-value="id"
class="w-full"
:show-clear="true"
:disabled="inlineEditSaving"
@change="saveInlineEdit(product, column)"
/>
</div>
<div
v-else
class="inline-edit-input-wrapper w-full"
Expand Down
Loading