feat(projects-bulk): wire 3 new dialogs into BulkActionsBar
Add RegionsBulkDialog / DaysBulkDialog / LimitBulkDialog to BulkActionsBar with open-state refs (regionsOpen/daysOpen/limitOpen), runBulk helper via store.bulkUpdate, and flex-wrap layout. Update spec: fix existing tests (bulkAction → bulkUpdate), add 3 new dialog-wiring tests (7/7 pass; full suite 614+3skipped/0failed). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,40 +1,91 @@
|
||||
<template>
|
||||
<v-card class="bulk-actions-bar" elevation="6">
|
||||
<DevIndexBadge :index="20" label="BulkActionsBar" :dialog-mode="true" style="top: 4px; right: 4px;" />
|
||||
<v-card-text class="d-flex align-center gap-3">
|
||||
<v-card-text class="d-flex align-center gap-3 flex-wrap">
|
||||
<strong>Выбрано: {{ store.selectedIds.size }}</strong>
|
||||
|
||||
<v-divider vertical />
|
||||
|
||||
<v-btn color="primary" variant="outlined" data-testid="bulk-regions" @click="regionsOpen = true">
|
||||
🌍 Регионы…
|
||||
</v-btn>
|
||||
<v-btn color="primary" variant="outlined" data-testid="bulk-days" @click="daysOpen = true">
|
||||
📅 Дни сбора…
|
||||
</v-btn>
|
||||
<v-btn color="primary" variant="outlined" data-testid="bulk-limit" @click="limitOpen = true">
|
||||
🎯 Лимит лидов…
|
||||
</v-btn>
|
||||
|
||||
<v-divider vertical />
|
||||
|
||||
<v-btn color="warning" prepend-icon="mdi-pause" data-testid="bulk-pause" @click="confirmAndRun('pause')">
|
||||
Приостановить
|
||||
</v-btn>
|
||||
<v-btn color="success" prepend-icon="mdi-play" data-testid="bulk-resume" @click="confirmAndRun('resume')">
|
||||
Возобновить
|
||||
</v-btn>
|
||||
|
||||
<v-divider vertical />
|
||||
|
||||
<v-btn color="error" prepend-icon="mdi-archive" data-testid="bulk-archive" @click="confirmAndRun('archive')">
|
||||
Архивировать
|
||||
</v-btn>
|
||||
|
||||
<v-spacer />
|
||||
<v-btn variant="text" data-testid="bulk-clear" @click="store.clearSelection">Снять выбор</v-btn>
|
||||
</v-card-text>
|
||||
|
||||
<RegionsBulkDialog
|
||||
v-model="regionsOpen"
|
||||
:count="store.selectedIds.size"
|
||||
@apply="(p) => runBulk({ action: 'update_regions', ...p })"
|
||||
/>
|
||||
<DaysBulkDialog
|
||||
v-model="daysOpen"
|
||||
:count="store.selectedIds.size"
|
||||
@apply="(p) => runBulk({ action: 'update_days', ...p })"
|
||||
/>
|
||||
<LimitBulkDialog
|
||||
v-model="limitOpen"
|
||||
:count="store.selectedIds.size"
|
||||
@apply="(p) => runBulk({ action: 'update_limit', ...p })"
|
||||
/>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useProjectsStore } from '../../stores/projectsStore';
|
||||
import DevIndexBadge from '../DevIndexBadge.vue';
|
||||
import RegionsBulkDialog from './RegionsBulkDialog.vue';
|
||||
import DaysBulkDialog from './DaysBulkDialog.vue';
|
||||
import LimitBulkDialog from './LimitBulkDialog.vue';
|
||||
|
||||
const store = useProjectsStore();
|
||||
|
||||
const regionsOpen = ref(false);
|
||||
const daysOpen = ref(false);
|
||||
const limitOpen = ref(false);
|
||||
|
||||
const messages: Record<string, string> = {
|
||||
pause: 'Приостановить выбранные проекты?',
|
||||
resume: 'Возобновить выбранные проекты?',
|
||||
archive: 'Архивировать выбранные проекты?\nДействие необратимо в Plan 5 (восстановление потребует ручного запроса).',
|
||||
};
|
||||
|
||||
function confirmAndRun(action: 'pause' | 'resume' | 'archive') {
|
||||
async function confirmAndRun(action: 'pause' | 'resume' | 'archive') {
|
||||
if (!window.confirm(messages[action])) return;
|
||||
store.bulkAction(action);
|
||||
await runBulk({ action });
|
||||
}
|
||||
|
||||
async function runBulk(payload: Parameters<typeof store.bulkUpdate>[0]) {
|
||||
const result = await store.bulkUpdate(payload);
|
||||
if (result.skipped.length > 0) {
|
||||
window.alert(`Применено: ${result.updated}. Пропущено: ${result.skipped.length} (конфликт с уже доставленными лидами).`);
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ regionsOpen, daysOpen, limitOpen });
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -44,5 +95,6 @@ function confirmAndRun(action: 'pause' | 'resume' | 'archive') {
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 10;
|
||||
max-width: calc(100vw - 48px);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,14 +2,31 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
import { createVuetify } from 'vuetify';
|
||||
import axios from 'axios';
|
||||
|
||||
import BulkActionsBar from '../../resources/js/components/projects/BulkActionsBar.vue';
|
||||
import { useProjectsStore } from '../../resources/js/stores/projectsStore';
|
||||
|
||||
const factory = () =>
|
||||
mount(BulkActionsBar, {
|
||||
global: { plugins: [createVuetify()] },
|
||||
vi.mock('axios');
|
||||
|
||||
const defaultStubs = {
|
||||
VDialog: { template: '<div><slot /></div>' },
|
||||
DevIndexBadge: true,
|
||||
RegionsBulkDialog: true,
|
||||
DaysBulkDialog: true,
|
||||
LimitBulkDialog: true,
|
||||
};
|
||||
|
||||
const factory = () => {
|
||||
vi.mocked(axios.post).mockResolvedValue({ data: { updated: 0, skipped: [], warnings: [] } });
|
||||
vi.mocked(axios.get).mockResolvedValue({ data: { data: [], meta: { total: 0 } } });
|
||||
return mount(BulkActionsBar, {
|
||||
global: {
|
||||
plugins: [createVuetify()],
|
||||
stubs: defaultStubs,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
@@ -25,20 +42,20 @@ describe('BulkActionsBar', () => {
|
||||
expect(wrapper.text()).toContain('2');
|
||||
});
|
||||
|
||||
it('clicking Pause triggers store.bulkAction("pause") after confirm', async () => {
|
||||
it('clicking Pause triggers store.bulkUpdate("pause") after confirm', async () => {
|
||||
const store = useProjectsStore();
|
||||
store.selectedIds.add(1);
|
||||
const spy = vi.spyOn(store, 'bulkAction').mockResolvedValue();
|
||||
const spy = vi.spyOn(store, 'bulkUpdate').mockResolvedValue({ updated: 1, skipped: [], warnings: [] });
|
||||
window.confirm = vi.fn(() => true);
|
||||
const wrapper = factory();
|
||||
await wrapper.find('[data-testid="bulk-pause"]').trigger('click');
|
||||
expect(spy).toHaveBeenCalledWith('pause');
|
||||
expect(spy).toHaveBeenCalledWith({ action: 'pause' });
|
||||
});
|
||||
|
||||
it('cancel confirm — bulkAction NOT called', async () => {
|
||||
it('cancel confirm — bulkUpdate NOT called', async () => {
|
||||
const store = useProjectsStore();
|
||||
store.selectedIds.add(1);
|
||||
const spy = vi.spyOn(store, 'bulkAction').mockResolvedValue();
|
||||
const spy = vi.spyOn(store, 'bulkUpdate').mockResolvedValue({ updated: 0, skipped: [], warnings: [] });
|
||||
window.confirm = vi.fn(() => false);
|
||||
const wrapper = factory();
|
||||
await wrapper.find('[data-testid="bulk-pause"]').trigger('click');
|
||||
@@ -53,3 +70,61 @@ describe('BulkActionsBar', () => {
|
||||
expect(store.selectedIds.size).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('BulkActionsBar — extended', () => {
|
||||
it('renders 3 new dialog-open buttons (Regions/Days/Limit)', async () => {
|
||||
const { wrapper } = await (async () => {
|
||||
setActivePinia(createPinia());
|
||||
vi.mocked(axios.post).mockResolvedValue({ data: { updated: 1, skipped: [], warnings: [] } });
|
||||
vi.mocked(axios.get).mockResolvedValue({ data: { data: [], meta: { total: 0 } } });
|
||||
const store = useProjectsStore();
|
||||
store.selectedIds.add(1);
|
||||
store.selectedIds.add(2);
|
||||
const wrapper = mount(BulkActionsBar, {
|
||||
global: {
|
||||
plugins: [createVuetify()],
|
||||
stubs: defaultStubs,
|
||||
},
|
||||
});
|
||||
return { wrapper, store };
|
||||
})();
|
||||
expect(wrapper.find('[data-testid="bulk-regions"]').exists()).toBe(true);
|
||||
expect(wrapper.find('[data-testid="bulk-days"]').exists()).toBe(true);
|
||||
expect(wrapper.find('[data-testid="bulk-limit"]').exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('opens RegionsBulkDialog on regions button click', async () => {
|
||||
setActivePinia(createPinia());
|
||||
vi.mocked(axios.post).mockResolvedValue({ data: { updated: 1, skipped: [], warnings: [] } });
|
||||
vi.mocked(axios.get).mockResolvedValue({ data: { data: [], meta: { total: 0 } } });
|
||||
const store = useProjectsStore();
|
||||
store.selectedIds.add(1);
|
||||
store.selectedIds.add(2);
|
||||
const wrapper = mount(BulkActionsBar, {
|
||||
global: {
|
||||
plugins: [createVuetify()],
|
||||
stubs: defaultStubs,
|
||||
},
|
||||
});
|
||||
await wrapper.find('[data-testid="bulk-regions"]').trigger('click');
|
||||
expect((wrapper.vm as any).regionsOpen).toBe(true);
|
||||
});
|
||||
|
||||
it('keeps existing pause/resume/archive buttons', async () => {
|
||||
setActivePinia(createPinia());
|
||||
vi.mocked(axios.post).mockResolvedValue({ data: { updated: 1, skipped: [], warnings: [] } });
|
||||
vi.mocked(axios.get).mockResolvedValue({ data: { data: [], meta: { total: 0 } } });
|
||||
const store = useProjectsStore();
|
||||
store.selectedIds.add(1);
|
||||
store.selectedIds.add(2);
|
||||
const wrapper = mount(BulkActionsBar, {
|
||||
global: {
|
||||
plugins: [createVuetify()],
|
||||
stubs: defaultStubs,
|
||||
},
|
||||
});
|
||||
expect(wrapper.find('[data-testid="bulk-pause"]').exists()).toBe(true);
|
||||
expect(wrapper.find('[data-testid="bulk-resume"]').exists()).toBe(true);
|
||||
expect(wrapper.find('[data-testid="bulk-archive"]').exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user