Files
portal/app/resources/js/components/projects/BulkActionsBar.vue
T
Дмитрий e8d5025656 fix(projects): C5 — replace window.alert() with v-snackbar in BulkActionsBar
window.alert блокирует UI thread, не accessible (a11y), breaks браузерный
automation (Playwright/Selenium). Заменено на v-snackbar (timeout 6s,
color warning, location bottom-right, кнопка «Закрыть»). Текст идентичен:
«Применено: N. Пропущено: M (конфликт с уже доставленными лидами).»

+2 Vitest specs (snackbar opens / snackbar НЕ opens at skipped=0).
window.confirm для pause/resume/archive намеренно оставлен — это
deliberate blocking прерывание для деструктивных операций (UX-pattern).

Closes audit ID C5 from docs/superpowers/specs/2026-05-15-portal-audit-design.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 09:02:47 +03:00

125 lines
4.5 KiB
Vue

<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 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-snackbar
v-model="skipToastOpen"
:timeout="6000"
color="warning"
location="bottom right"
data-testid="bulk-skip-toast"
>
{{ skipToastText }}
<template #actions>
<v-btn variant="text" @click="skipToastOpen = false">Закрыть</v-btn>
</template>
</v-snackbar>
</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);
// Sprint 1 C5: window.alert → v-snackbar (non-blocking, accessible, не breaks браузерный automation).
const skipToastOpen = ref(false);
const skipToastText = ref('');
const messages: Record<string, string> = {
pause: 'Приостановить выбранные проекты?',
resume: 'Возобновить выбранные проекты?',
archive:
'Архивировать выбранные проекты?\nДействие необратимо в Plan 5 (восстановление потребует ручного запроса).',
};
async function confirmAndRun(action: 'pause' | 'resume' | 'archive') {
if (!window.confirm(messages[action])) return;
await runBulk({ action });
}
async function runBulk(payload: Parameters<typeof store.bulkUpdate>[0]) {
const result = await store.bulkUpdate(payload);
if (result.skipped.length > 0) {
skipToastText.value = `Применено: ${result.updated}. Пропущено: ${result.skipped.length} (конфликт с уже доставленными лидами).`;
skipToastOpen.value = true;
}
}
defineExpose({ regionsOpen, daysOpen, limitOpen, skipToastOpen, skipToastText, runBulk });
</script>
<style scoped>
.bulk-actions-bar {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
z-index: 10;
max-width: calc(100vw - 48px);
}
</style>