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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@

## Апрель 2026

### Улучшения (в разработке)

**Заказ / поля форм (#234):**
- Во вкладках «Информация» и «Адрес» при пустом наборе полей — отдельные лексиконы (`ms3_order_tab_*_model_fields_empty_hint`), ссылка «Настроить поля» на `?a=mgr/utilities&namespace=minishop3&tab=ms3-utilities-model-fields-tab&model=…` (тот же `a`, что в меню MS3), в т.ч. в режиме создания заказа на вкладке «Информация».
- Ext-утилиты: deep link по `tab=` и/или `model=`; в коде зафиксирована связка id вкладки с `MS3_UTILITIES_MODEL_FIELDS_TAB_ID` (`managerModelFieldsUrl.js`); `ModelFieldsGrid` — предвыбор модели из allowlist API, синхронизация URL (`tab` + `model`), удаление невалидного `model` из адресной строки.
- `aria-label` на ссылках; `ms3_model_fields_empty` остаётся для экрана настройки полей, не для карточки заказа.

### [2026-04-27] 🚀 Версия 1.10.1-beta1

**Тип релиза:** PATCH (beta) — точечные исправления и восстановление контракта событий
Expand Down Expand Up @@ -49,6 +56,9 @@
**ServiceRegistry — лишний debug-шум в логах (#225, closes #224):**
- На штатной установке без кастомизации сервисов `loadMainConfig()` / `loadAddonConfigs()` писали DEBUG про отсутствие дефолтных override-путей. Теперь логирование срабатывает только если оператор явно задал `ms3_services_config` / `ms3_services_addons_dir` через system settings, а файла/папки по этому пути нет.

**Карточка заказа (Vue) — кнопки «Сохранить»/«Отмена» при пустом наборе полей (#182):**
- На вкладках «Информация о заказе» и «Адрес» панель действий скрывается, если нет настроенных полей формы; в режиме создания заказа и для черновика с блоком клиента кнопки по-прежнему показываются.

#### 📁 Изменённые файлы

```
Expand All @@ -60,6 +70,8 @@ core/components/minishop3/src/Controllers/Api/Manager/OrdersController.php
core/components/minishop3/src/Processors/Utilities/Import/Fields.php
core/components/minishop3/src/ServiceRegistry.php
core/components/minishop3/src/Services/Order/OrderFinalizeService.php
vueManager/src/components/order/OrderAddressTab.vue
vueManager/src/components/order/OrderInfoTab.vue
vueManager/src/components/product/ProductOptionsTab.vue
```

Expand Down
26 changes: 23 additions & 3 deletions assets/components/minishop3/js/mgr/utilities/utilities.panel.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,32 @@ ms3.panel.Utilities = function (config) {
deferredRender: false,
listeners: {
afterrender: function(panel) {
var savedId = localStorage.getItem('ms3-utilities-active-tab');
if (savedId) {
var sp = new URLSearchParams(window.location.search);
var tabParam = sp.get("tab");
// Model fields tab id must stay in sync with Vue:
// vueManager/src/utils/managerModelFieldsUrl.js → MS3_UTILITIES_MODEL_FIELDS_TAB_ID
// and items[].id below (ms3-utilities-model-fields-tab). #234 deep link ?tab= / ?model=
var modelFieldsTab = Ext.getCmp(
"ms3-utilities-model-fields-tab",
);
// Deep link: ?model=… and/or ?tab=ms3-utilities-model-fields-tab (#234)
if (
sp.get("model") ||
tabParam === "ms3-utilities-model-fields-tab"
) {
if (modelFieldsTab) {
panel.setActiveTab(modelFieldsTab);
}
} else {
var savedId = localStorage.getItem(
"ms3-utilities-active-tab",
);
if (savedId) {
var tab = Ext.getCmp(savedId);
if (tab) {
panel.setActiveTab(tab);
panel.setActiveTab(tab);
}
}
}
// Enable saving only after initial restore
panel.tabStateReady = true;
Expand Down
5 changes: 5 additions & 0 deletions core/components/minishop3/lexicon/en/vue.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,11 @@
$_lang['ms3_model_field_hidden'] = 'Field is hidden';
$_lang['ms3_model_field_order_updated'] = 'Field order updated';
$_lang['ms3_model_fields_empty'] = 'No fields found. Add your first field.';
$_lang['ms3_order_tab_info_model_fields_empty_hint'] = 'No order form fields are configured for this tab.';
$_lang['ms3_order_tab_address_model_fields_empty_hint'] = 'No address form fields are configured for this tab.';
$_lang['ms3_order_open_model_fields_settings'] = 'Configure fields';
$_lang['ms3_order_open_model_fields_settings_aria'] = 'Open Utilities, Form fields tab, order (msOrder) model';
$_lang['ms3_order_open_model_fields_address_aria'] = 'Open Utilities, Form fields tab, order address (msOrderAddress) model';

// Model names for dropdown
$_lang['ms3_model_order'] = 'Order';
Expand Down
5 changes: 5 additions & 0 deletions core/components/minishop3/lexicon/ru/vue.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,11 @@
$_lang['ms3_model_field_hidden'] = 'Поле скрыто';
$_lang['ms3_model_field_order_updated'] = 'Порядок полей обновлён';
$_lang['ms3_model_fields_empty'] = 'Поля не найдены. Добавьте первое поле.';
$_lang['ms3_order_tab_info_model_fields_empty_hint'] = 'Для этой вкладки не настроены поля формы заказа.';
$_lang['ms3_order_tab_address_model_fields_empty_hint'] = 'Для этой вкладки не настроены поля адреса.';
$_lang['ms3_order_open_model_fields_settings'] = 'Настроить поля';
$_lang['ms3_order_open_model_fields_settings_aria'] = 'Открыть утилиты MiniShop3, вкладка «Поля форм», модель заказа (msOrder)';
$_lang['ms3_order_open_model_fields_address_aria'] = 'Открыть утилиты MiniShop3, вкладка «Поля форм», модель адреса (msOrderAddress)';

// Model names for dropdown
$_lang['ms3_model_order'] = 'Заказ';
Expand Down
45 changes: 42 additions & 3 deletions vueManager/src/components/ModelFieldsGrid.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ import { computed, onMounted, ref } from 'vue'
import draggable from 'vuedraggable'

import request from '../request.js'
import {
getModelQueryParam,
MS3_UTILITIES_MODEL_FIELDS_TAB_ID,
} from '../utils/managerModelFieldsUrl.js'

const toast = useToast()
const confirm = useConfirm()
Expand Down Expand Up @@ -84,14 +88,38 @@ const sectionOptions = computed(() => {
/**
* Load available models
*/
function removeInvalidModelFromUrl(list) {
if (typeof window === 'undefined') {
return
}
const raw = getModelQueryParam()
if (!raw || !list.length) {
return
}
if (list.some(m => m.value === raw)) {
return
}
const u = new URL(window.location.href)
u.searchParams.delete('model')
window.history.replaceState({}, '', `${u.pathname}${u.search}${u.hash}`)
}

async function loadModels() {
try {
const response = await request.get('/api/mgr/model-fields/models')
if (response && response.models) {
models.value = response.models
// Set default filter to first model
if (models.value.length > 0 && !filterModel.value) {
filterModel.value = models.value[0].value
if (models.value.length > 0) {
const fromUrl = getModelQueryParam()
const allowed = models.value.some(m => m.value === fromUrl)
if (fromUrl && !allowed) {
removeInvalidModelFromUrl(models.value)
}
if (fromUrl && allowed) {
filterModel.value = fromUrl
} else if (!filterModel.value) {
filterModel.value = models.value[0].value
}
}
}
} catch (error) {
Expand Down Expand Up @@ -167,7 +195,18 @@ async function loadSections() {
}
}

function syncModelQueryToUrl() {
if (typeof window === 'undefined' || !filterModel.value) {
return
}
const u = new URL(window.location.href)
u.searchParams.set('tab', MS3_UTILITIES_MODEL_FIELDS_TAB_ID)
u.searchParams.set('model', filterModel.value)
window.history.replaceState({}, '', `${u.pathname}${u.search}${u.hash}`)
}

function onModelChange() {
syncModelQueryToUrl()
loadFields()
loadSections()
}
Expand Down
51 changes: 36 additions & 15 deletions vueManager/src/components/order/OrderAddressTab.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import InputNumber from 'primevue/inputnumber'
import InputText from 'primevue/inputtext'
import Select from 'primevue/select'
import Textarea from 'primevue/textarea'
import { inject } from 'vue'
import { computed, inject } from 'vue'

import { ORDER_CONTEXT_KEY } from '../../composables/orderContext.js'
import { buildManagerModelFieldsSettingsUrl, MS3_MODEL_ORDER_ADDRESS } from '../../utils/managerModelFieldsUrl.js'
import OrderFormActionsBar from './OrderFormActionsBar.vue'

const selectedCustomer = defineModel('selectedCustomer', {
type: Object,
Expand All @@ -21,7 +23,7 @@ const createCustomerFromData = defineModel('createCustomerFromData', {
default: false,
})

defineProps({
const props = defineProps({
addressFieldsBySection: { type: Array, required: true },
})

Expand Down Expand Up @@ -53,6 +55,22 @@ const {
} = orderCtx

const { _ } = useLexicon()

const hasAddressFieldSections = computed(
() => (props.addressFieldsBySection?.length ?? 0) > 0
)

/**
* Скрыть «Сохранить»/«Отмена», если нет полей адреса и нет сценария с клиентом (#182):
* create / draft (блок выбора клиента) / есть секции полей.
*/
const showAddressTabActions = computed(
() => isCreateMode.value || isDraft.value || hasAddressFieldSections.value
)

const addressModelFieldsSettingsUrl = computed(() =>
buildManagerModelFieldsSettingsUrl(MS3_MODEL_ORDER_ADDRESS)
)
</script>

<template>
Expand Down Expand Up @@ -206,21 +224,24 @@ const { _ } = useLexicon()
</template>

<div v-if="addressFieldsBySection.length === 0" class="no-fields-message">
<p>{{ _('ms3_model_fields_empty') }}</p>
<p>{{ _('ms3_order_tab_address_model_fields_empty_hint') }}</p>
<a
:href="addressModelFieldsSettingsUrl"
class="ms3-model-fields-link"
:aria-label="_('ms3_order_open_model_fields_address_aria')"
>
{{ _('ms3_order_open_model_fields_settings') }}
</a>
</div>

<div class="actions-bar mt-3">
<Button
v-if="isCreateMode"
:label="_('ms3_order_create')"
icon="pi pi-plus"
severity="success"
:loading="saving"
@click="createOrder"
/>
<Button v-else :label="_('save')" icon="pi pi-check" :loading="saving" @click="saveOrder" />
<Button :label="_('cancel')" icon="pi pi-times" severity="secondary" @click="goBack" />
</div>
<OrderFormActionsBar
v-if="showAddressTabActions"
:is-create-mode="isCreateMode"
:saving="saving"
@create="createOrder"
@save="saveOrder"
@cancel="goBack"
/>
</div>
</template>

Expand Down
40 changes: 40 additions & 0 deletions vueManager/src/components/order/OrderFormActionsBar.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<script setup>
import { useLexicon } from '@vuetools/useLexicon'
import Button from 'primevue/button'

defineProps({
isCreateMode: { type: Boolean, required: true },
saving: { type: Boolean, default: false },
})

const emit = defineEmits(['create', 'save', 'cancel'])
const { _ } = useLexicon()
</script>

<template>
<div class="actions-bar mt-3">
<Button
v-if="isCreateMode"
:label="_('ms3_order_create')"
icon="pi pi-plus"
severity="success"
:loading="saving"
@click="emit('create')"
/>
<Button
v-else
:label="_('save')"
icon="pi pi-check"
:loading="saving"
@click="emit('save')"
/>
<Button
:label="_('cancel')"
icon="pi pi-times"
severity="secondary"
@click="emit('cancel')"
/>
</div>
</template>

<style scoped src="./orderFieldsLayout.css"></style>
48 changes: 33 additions & 15 deletions vueManager/src/components/order/OrderInfoTab.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ import InputText from 'primevue/inputtext'
import Message from 'primevue/message'
import Select from 'primevue/select'
import Textarea from 'primevue/textarea'
import { inject } from 'vue'
import { computed, inject } from 'vue'

import { ORDER_CONTEXT_KEY } from '../../composables/orderContext.js'
import { buildManagerModelFieldsSettingsUrl, MS3_MODEL_ORDER } from '../../utils/managerModelFieldsUrl.js'
import OrderFormActionsBar from './OrderFormActionsBar.vue'

defineProps({
const props = defineProps({
orderFieldsBySection: { type: Array, required: true },
})

Expand Down Expand Up @@ -45,6 +47,19 @@ const {
} = orderCtx

const { _ } = useLexicon()

const hasOrderFieldSections = computed(
() => (props.orderFieldsBySection?.length ?? 0) > 0
)

/** Скрыть «Сохранить»/«Отмена», если нет полей заказа (#182); в режиме создания кнопки нужны. */
const showOrderInfoActions = computed(
() => isCreateMode.value || hasOrderFieldSections.value
)

const orderModelFieldsSettingsUrl = computed(() =>
buildManagerModelFieldsSettingsUrl(MS3_MODEL_ORDER)
)
</script>

<template>
Expand Down Expand Up @@ -177,7 +192,14 @@ const { _ } = useLexicon()
</template>

<div v-if="orderFieldsBySection.length === 0" class="no-fields-message">
<p>{{ _('ms3_model_fields_empty') }}</p>
<p>{{ _('ms3_order_tab_info_model_fields_empty_hint') }}</p>
<a
:href="orderModelFieldsSettingsUrl"
class="ms3-model-fields-link"
:aria-label="_('ms3_order_open_model_fields_settings_aria')"
>
{{ _('ms3_order_open_model_fields_settings') }}
</a>
</div>

<Message
Expand All @@ -202,18 +224,14 @@ const { _ } = useLexicon()
</div>
</Message>

<div class="actions-bar mt-3">
<Button
v-if="isCreateMode"
:label="_('ms3_order_create')"
icon="pi pi-plus"
severity="success"
:loading="saving"
@click="createOrder"
/>
<Button v-else :label="_('save')" icon="pi pi-check" :loading="saving" @click="saveOrder" />
<Button :label="_('cancel')" icon="pi pi-times" severity="secondary" @click="goBack" />
</div>
<OrderFormActionsBar
v-if="showOrderInfoActions"
:is-create-mode="isCreateMode"
:saving="saving"
@create="createOrder"
@save="saveOrder"
@cancel="goBack"
/>
</div>
</template>

Expand Down
13 changes: 13 additions & 0 deletions vueManager/src/components/order/orderFieldsLayout.css
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,19 @@
font-style: italic;
}

.no-fields-message .ms3-model-fields-link {
display: inline-block;
margin-top: 0.5rem;
color: var(--ms3-accent-primary);
font-style: normal;
font-weight: 500;
text-decoration: underline;
}

.no-fields-message .ms3-model-fields-link:hover {
color: var(--ms3-text-accent-dark, var(--ms3-accent-primary));
}

.actions-bar {
display: flex;
gap: 0.5rem;
Expand Down
Loading