feat(deals): DealsBulkBar — status-change only (drop export/delete/trash)
This commit is contained in:
@@ -1,20 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* Sticky-bar bulk-actions для выбранных сделок (Sprint 3 Phase C).
|
||||
*
|
||||
* Показывается когда selectedCount > 0. В trash-mode — только кнопка
|
||||
* «Восстановить»; в обычном режиме — Сменить статус (menu со списком),
|
||||
* Экспорт, Удалить.
|
||||
*
|
||||
* Контракт: stateless presentation — родитель держит `selected`, `statusMenuOpen`,
|
||||
* `leadStatuses`, передаёт через props и слушает emit'ы.
|
||||
* Sticky-bar массовой смены статуса для выбранных сделок (редизайн 2026-05-17).
|
||||
* Только смена статуса — корзина/экспорт убраны (экспорт — панель по датам).
|
||||
*/
|
||||
import type { MockDeal } from '../../composables/mockDeals';
|
||||
import type { LeadStatus } from '../../composables/leadStatuses';
|
||||
|
||||
defineProps<{
|
||||
selectedCount: number;
|
||||
trashMode: boolean;
|
||||
statusMenuOpen: boolean;
|
||||
leadStatuses: LeadStatus[];
|
||||
}>();
|
||||
@@ -22,9 +15,6 @@ defineProps<{
|
||||
defineEmits<{
|
||||
'update:statusMenuOpen': [value: boolean];
|
||||
'apply-status': [slug: MockDeal['statusSlug']];
|
||||
'apply-export': [];
|
||||
'request-delete': [];
|
||||
'apply-restore-trash': [];
|
||||
'clear-selected': [];
|
||||
}>();
|
||||
</script>
|
||||
@@ -39,73 +29,38 @@ defineEmits<{
|
||||
data-testid="bulk-bar"
|
||||
>
|
||||
<div class="bulk-bar-inner">
|
||||
<span class="bulk-count">
|
||||
Выбрано <span class="num">{{ selectedCount }}</span>
|
||||
</span>
|
||||
<span class="bulk-count">Выбрано <span class="num">{{ selectedCount }}</span></span>
|
||||
<v-spacer />
|
||||
<!-- В trash-mode только Восстановить; в обычном режиме — полный набор. -->
|
||||
<v-btn
|
||||
v-if="trashMode"
|
||||
variant="tonal"
|
||||
color="success"
|
||||
size="small"
|
||||
prepend-icon="mdi-restore"
|
||||
data-testid="bulk-restore-trash-btn"
|
||||
@click="$emit('apply-restore-trash')"
|
||||
<v-menu
|
||||
:model-value="statusMenuOpen"
|
||||
:close-on-content-click="false"
|
||||
@update:model-value="(v: boolean) => $emit('update:statusMenuOpen', v)"
|
||||
>
|
||||
Восстановить
|
||||
</v-btn>
|
||||
<template v-if="!trashMode">
|
||||
<v-menu
|
||||
:model-value="statusMenuOpen"
|
||||
:close-on-content-click="false"
|
||||
@update:model-value="(v: boolean) => $emit('update:statusMenuOpen', v)"
|
||||
>
|
||||
<template #activator="{ props: menuProps }">
|
||||
<v-btn
|
||||
v-bind="menuProps"
|
||||
variant="tonal"
|
||||
size="small"
|
||||
prepend-icon="mdi-tag-arrow-right"
|
||||
data-testid="bulk-status-btn"
|
||||
>
|
||||
Сменить статус
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list density="compact" max-height="320" min-width="240">
|
||||
<v-list-item
|
||||
v-for="s in leadStatuses"
|
||||
:key="s.slug"
|
||||
:data-testid="`bulk-status-item-${s.slug}`"
|
||||
@click="$emit('apply-status', s.slug)"
|
||||
>
|
||||
<template #prepend>
|
||||
<span class="status-dot" :style="{ background: s.colorHex }" />
|
||||
</template>
|
||||
<v-list-item-title>{{ s.nameRu }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<v-btn
|
||||
variant="tonal"
|
||||
size="small"
|
||||
prepend-icon="mdi-download"
|
||||
data-testid="bulk-export-btn"
|
||||
@click="$emit('apply-export')"
|
||||
>
|
||||
Экспорт
|
||||
</v-btn>
|
||||
<v-btn
|
||||
variant="tonal"
|
||||
color="error"
|
||||
size="small"
|
||||
prepend-icon="mdi-trash-can-outline"
|
||||
data-testid="bulk-delete-btn"
|
||||
@click="$emit('request-delete')"
|
||||
>
|
||||
Удалить
|
||||
</v-btn>
|
||||
</template>
|
||||
<template #activator="{ props: menuProps }">
|
||||
<v-btn
|
||||
v-bind="menuProps"
|
||||
variant="tonal"
|
||||
size="small"
|
||||
prepend-icon="mdi-tag-arrow-right"
|
||||
data-testid="bulk-status-btn"
|
||||
>
|
||||
Сменить статус
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list density="compact" max-height="320" min-width="240">
|
||||
<v-list-item
|
||||
v-for="s in leadStatuses"
|
||||
:key="s.slug"
|
||||
:data-testid="`bulk-status-item-${s.slug}`"
|
||||
@click="$emit('apply-status', s.slug)"
|
||||
>
|
||||
<template #prepend>
|
||||
<span class="status-dot" :style="{ background: s.colorHex }" />
|
||||
</template>
|
||||
<v-list-item-title>{{ s.nameRu }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<v-btn
|
||||
icon="mdi-close"
|
||||
variant="text"
|
||||
@@ -123,7 +78,6 @@ defineEmits<{
|
||||
font-feature-settings: 'tnum';
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
@@ -131,7 +85,6 @@ defineEmits<{
|
||||
border-radius: 50%;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.bulk-bar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { createVuetify } from 'vuetify';
|
||||
import DealsBulkBar from '../../resources/js/components/deals/DealsBulkBar.vue';
|
||||
|
||||
const vuetify = createVuetify();
|
||||
const leadStatuses = [
|
||||
{ slug: 'new', nameRu: 'Новые', isSystem: true, sortOrder: 1, colorHex: '#3B82F6' },
|
||||
{ slug: 'viewed', nameRu: 'Просмотрено', isSystem: true, sortOrder: 2, colorHex: '#8B5CF6' },
|
||||
];
|
||||
|
||||
describe('DealsBulkBar', () => {
|
||||
it('скрыт при selectedCount=0', () => {
|
||||
const w = mount(DealsBulkBar, {
|
||||
props: { selectedCount: 0, statusMenuOpen: false, leadStatuses },
|
||||
global: { plugins: [vuetify] },
|
||||
});
|
||||
expect(w.find('[data-testid="bulk-bar"]').exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('виден при selectedCount>0 и показывает количество', () => {
|
||||
const w = mount(DealsBulkBar, {
|
||||
props: { selectedCount: 3, statusMenuOpen: false, leadStatuses },
|
||||
global: { plugins: [vuetify] },
|
||||
});
|
||||
const bar = w.find('[data-testid="bulk-bar"]');
|
||||
expect(bar.exists()).toBe(true);
|
||||
expect(bar.text()).toContain('3');
|
||||
});
|
||||
|
||||
it('НЕ содержит кнопок Экспорт/Удалить', () => {
|
||||
const w = mount(DealsBulkBar, {
|
||||
props: { selectedCount: 2, statusMenuOpen: false, leadStatuses },
|
||||
global: { plugins: [vuetify] },
|
||||
});
|
||||
expect(w.find('[data-testid="bulk-export-btn"]').exists()).toBe(false);
|
||||
expect(w.find('[data-testid="bulk-delete-btn"]').exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('✕ эмитит clear-selected', async () => {
|
||||
const w = mount(DealsBulkBar, {
|
||||
props: { selectedCount: 2, statusMenuOpen: false, leadStatuses },
|
||||
global: { plugins: [vuetify] },
|
||||
});
|
||||
await w.find('[data-testid="bulk-clear-btn"]').trigger('click');
|
||||
expect(w.emitted('clear-selected')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user