refactor(deals): C2 review fixes — watcher-driven draft + named toggles
Code-review fixes для commit 4e77947 (C2 FilterChip popovers):
I-4 (latent interaction bug): Удалена двойная open-path в FilterChip
activator. v-menu сам управляет projectMenuOpen/managerMenuOpen через
activatorProps. Draft-state копируется при menu open → true через
watch(menuOpen, ...). Раньше:
- Activator click: menuOpen=true
- @click on FilterChip: onRedesignFilterClick → menuOpen=true (duplicate)
- Re-click для close: activator toggles false → onRedesignFilterClick
forces true back → menu не закрывается.
I-2 (inline toggle extract): Multi-line ternary @click заменён на
named methods toggleProjectDraft(proj) / toggleManagerDraft(name).
Консистентно с existing clearProjectDraft / clearManagerDraft. Также
unit-testable независимо от template.
onRedesignFilterClick остаётся для Status chip read-only behavior (P2
backlog Sprint 5). defineExpose обновлён: убран onRedesignFilterClick,
добавлены toggleProjectDraft/toggleManagerDraft/clearProjectDraft/
clearManagerDraft (для будущих spec'ов).
Vitest 3/3 C2-specs обновлены на прямой trigger projectMenuOpen=true
+ $nextTick (watcher seeds draft). Регрессий 0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,7 +13,7 @@
|
||||
*
|
||||
* Источник статусов — composables/leadStatuses.ts (snapshot из db/schema.sql:2130).
|
||||
*/
|
||||
import { computed, defineAsyncComponent, onMounted, reactive, ref } from 'vue';
|
||||
import { computed, defineAsyncComponent, onMounted, reactive, ref, watch } from 'vue';
|
||||
import { DEALS_TABS, MOCK_DEALS, type MockDeal } from '../composables/mockDeals';
|
||||
import { mapApiDeal } from '../composables/dealsApiMapper';
|
||||
import { usePolling } from '../composables/usePolling';
|
||||
@@ -38,22 +38,30 @@ import { buildCsvString, triggerBlobDownload, triggerCsvDownload } from '../comp
|
||||
const { rowHeight } = useDensity();
|
||||
|
||||
// Sprint 1 C2: popovers для Проект/Менеджер chip'ов. Draft-state накапливает
|
||||
// выбор в v-menu, при «Применить» переносится в filterProjects/filterManagers.
|
||||
// Status chip остаётся read-only (P2 backlog в Sprint 5).
|
||||
// выбор внутри v-menu; копируется из filterProjects/filterManagers при открытии
|
||||
// (watch на menu open=true); переносится обратно в filterProjects/Managers на
|
||||
// «Применить» button. Status chip — read-only (P2 backlog Sprint 5).
|
||||
const projectMenuOpen = ref(false);
|
||||
const managerMenuOpen = ref(false);
|
||||
const projectMenuDraft = ref<string[]>([]);
|
||||
const managerMenuDraft = ref<string[]>([]);
|
||||
|
||||
// При открытии меню — копируем текущий filter в draft (snapshot-on-open).
|
||||
// При закрытии без apply — draft остаётся, но не влияет на filterProjects
|
||||
// (apply нужен явно).
|
||||
watch(projectMenuOpen, (isOpen) => {
|
||||
if (isOpen) projectMenuDraft.value = [...filterProjects.value];
|
||||
});
|
||||
watch(managerMenuOpen, (isOpen) => {
|
||||
if (isOpen) managerMenuDraft.value = [...filterManagers.value];
|
||||
});
|
||||
|
||||
function onRedesignFilterClick(name: string): void {
|
||||
if (name === 'Проект') {
|
||||
projectMenuDraft.value = [...filterProjects.value];
|
||||
projectMenuOpen.value = true;
|
||||
} else if (name === 'Менеджер') {
|
||||
managerMenuDraft.value = [...filterManagers.value];
|
||||
managerMenuOpen.value = true;
|
||||
// Status chip — read-only summary (P2 backlog Sprint 5).
|
||||
// Project/Manager managed by v-menu activator (no manual click handler needed).
|
||||
if (name === 'Статус') {
|
||||
// no-op — placeholder для будущей реализации
|
||||
}
|
||||
// 'Статус' — read-only summary, popover откроется в Sprint 5 (P2).
|
||||
}
|
||||
|
||||
function applyProjectFilter(): void {
|
||||
@@ -74,6 +82,18 @@ function clearManagerDraft(): void {
|
||||
managerMenuDraft.value = [];
|
||||
}
|
||||
|
||||
function toggleProjectDraft(proj: string): void {
|
||||
projectMenuDraft.value = projectMenuDraft.value.includes(proj)
|
||||
? projectMenuDraft.value.filter((p) => p !== proj)
|
||||
: [...projectMenuDraft.value, proj];
|
||||
}
|
||||
|
||||
function toggleManagerDraft(name: string): void {
|
||||
managerMenuDraft.value = managerMenuDraft.value.includes(name)
|
||||
? managerMenuDraft.value.filter((m) => m !== name)
|
||||
: [...managerMenuDraft.value, name];
|
||||
}
|
||||
|
||||
const auth = useAuthStore();
|
||||
const leadStatusesStore = useLeadStatusesStore();
|
||||
|
||||
@@ -386,7 +406,10 @@ defineExpose({
|
||||
managerMenuDraft,
|
||||
applyProjectFilter,
|
||||
applyManagerFilter,
|
||||
onRedesignFilterClick,
|
||||
clearProjectDraft,
|
||||
clearManagerDraft,
|
||||
toggleProjectDraft,
|
||||
toggleManagerDraft,
|
||||
});
|
||||
|
||||
const leadStatuses = computed(() => leadStatusesStore.statuses);
|
||||
@@ -518,7 +541,6 @@ const waitingPay = computed(() => dealsState.filter((d) => d.statusSlug === 'wai
|
||||
label="Проект"
|
||||
:count="filterProjects.length"
|
||||
:active="filterProjects.length > 0"
|
||||
@click="onRedesignFilterClick('Проект')"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
@@ -532,11 +554,7 @@ const waitingPay = computed(() => dealsState.filter((d) => d.statusSlug === 'wai
|
||||
v-for="proj in availableProjects"
|
||||
:key="proj"
|
||||
class="py-1"
|
||||
@click="
|
||||
projectMenuDraft.includes(proj)
|
||||
? (projectMenuDraft = projectMenuDraft.filter((p) => p !== proj))
|
||||
: (projectMenuDraft = [...projectMenuDraft, proj])
|
||||
"
|
||||
@click="toggleProjectDraft(proj)"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-checkbox-btn :model-value="projectMenuDraft.includes(proj)" />
|
||||
@@ -569,7 +587,6 @@ const waitingPay = computed(() => dealsState.filter((d) => d.statusSlug === 'wai
|
||||
label="Менеджер"
|
||||
:count="filterManagers.length"
|
||||
:active="filterManagers.length > 0"
|
||||
@click="onRedesignFilterClick('Менеджер')"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
@@ -583,11 +600,7 @@ const waitingPay = computed(() => dealsState.filter((d) => d.statusSlug === 'wai
|
||||
v-for="mgr in availableManagers"
|
||||
:key="mgr.name"
|
||||
class="py-1"
|
||||
@click="
|
||||
managerMenuDraft.includes(mgr.name)
|
||||
? (managerMenuDraft = managerMenuDraft.filter((m) => m !== mgr.name))
|
||||
: (managerMenuDraft = [...managerMenuDraft, mgr.name])
|
||||
"
|
||||
@click="toggleManagerDraft(mgr.name)"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-checkbox-btn :model-value="managerMenuDraft.includes(mgr.name)" />
|
||||
|
||||
@@ -67,8 +67,9 @@ describe('FilterChip popovers (Sprint 1 C2)', () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const vm = wrapper.vm as any;
|
||||
expect(vm.projectMenuOpen).toBe(false);
|
||||
// Trigger chip click via exposed handler
|
||||
vm.onRedesignFilterClick('Проект');
|
||||
// Trigger via direct ref assignment (v-menu activator manages this ref).
|
||||
// watch(projectMenuOpen) will seed projectMenuDraft on open=true.
|
||||
vm.projectMenuOpen = true;
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(vm.projectMenuOpen).toBe(true);
|
||||
});
|
||||
@@ -84,7 +85,9 @@ describe('FilterChip popovers (Sprint 1 C2)', () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const vm = wrapper.vm as any;
|
||||
expect(vm.managerMenuOpen).toBe(false);
|
||||
vm.onRedesignFilterClick('Менеджер');
|
||||
// Trigger via direct ref assignment (v-menu activator manages this ref).
|
||||
// watch(managerMenuOpen) will seed managerMenuDraft on open=true.
|
||||
vm.managerMenuOpen = true;
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(vm.managerMenuOpen).toBe(true);
|
||||
});
|
||||
@@ -99,6 +102,9 @@ describe('FilterChip popovers (Sprint 1 C2)', () => {
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const vm = wrapper.vm as any;
|
||||
// Open menu (watch seeds draft from filterProjects), then override draft manually.
|
||||
vm.projectMenuOpen = true;
|
||||
await wrapper.vm.$nextTick();
|
||||
vm.projectMenuDraft = ['demo-project-1', 'demo-project-2'];
|
||||
vm.applyProjectFilter();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
Reference in New Issue
Block a user