import type { Meta, StoryObj } from '@storybook/vue3-vite' import { computed, ref } from 'vue' import { mdiChevronLeft, mdiChevronRight, mdiDelete, mdiPencil } from '@mdi/js' import type { VDataTable } from 'vuetify/components' import SyTable from './SyTable.vue' import DialogBox from '@/components/DialogBox/DialogBox.vue' import SyTextField from '@/components/Customs/SyTextField/SyTextField.vue' import SySelect from '@/components/Customs/Selects/SySelect/SySelect.vue' const meta = { title: 'Composants/Tableaux/SyTable/Édition/Actions groupées', component: SyTable, decorators: [ () => ({ template: '
', }), ], parameters: { layout: 'fullscreen', controls: { hideNoControlsWarning: true }, }, argTypes: { 'bulk-actions': { description: 'Barre affichée quand des lignes sont sélectionnées. Le composant ne fournit que la sélection ; **le projet rend ses propres actions** (éditer, supprimer, exporter…) et pilote leur UX (DialogBox, drawer, page…).', control: undefined, table: { category: 'slots', type: { summary: 'slot', detail: '{ selected: Record[], count: number, clearSelection: () => void }' }, }, }, }, } satisfies Meta export default meta type Story = StoryObj const headers = [ { title: 'Nom', key: 'lastname' }, { title: 'Prénom', key: 'firstname' }, { title: 'Email', key: 'email' }, { title: 'Statut', key: 'statut' }, ] const baseItems = [ { id: 1, firstname: 'Virginie', lastname: 'Beauchesne', email: 'virginie.beauchesne@example.com', statut: 'Actif' }, { id: 2, firstname: 'Étienne', lastname: 'Salois', email: 'etienne.salois@example.com', statut: 'Inactif' }, { id: 3, firstname: 'Camille', lastname: 'Tremblay', email: 'camille.tremblay@example.com', statut: 'Actif' }, ] // Jeu plus long + conteneur scrollable pour illustrer la barre d'actions // « sticky » : elle reste visible en haut même après avoir sélectionné une // ligne située plus bas dans le tableau. const manyItems = [ { id: 1, firstname: 'Virginie', lastname: 'Beauchesne', email: 'virginie.beauchesne@example.com', statut: 'Actif' }, { id: 2, firstname: 'Étienne', lastname: 'Salois', email: 'etienne.salois@example.com', statut: 'Inactif' }, { id: 3, firstname: 'Camille', lastname: 'Tremblay', email: 'camille.tremblay@example.com', statut: 'Actif' }, { id: 4, firstname: 'Thierry', lastname: 'Bobu', email: 'thierry.bobu@example.com', statut: 'Actif' }, { id: 5, firstname: 'Bernadette', lastname: 'Langelier', email: 'bernadette.langelier@example.com', statut: 'Suspendu' }, { id: 6, firstname: 'Agate', lastname: 'Roy', email: 'agate.roy@example.com', statut: 'Actif' }, { id: 7, firstname: 'Louis', lastname: 'Denis', email: 'louis.denis@example.com', statut: 'Inactif' }, { id: 8, firstname: 'Édith', lastname: 'Cartier', email: 'edith.cartier@example.com', statut: 'Actif' }, { id: 9, firstname: 'Alphonse', lastname: 'Bouvier', email: 'alphonse.bouvier@example.com', statut: 'Suspendu' }, { id: 10, firstname: 'Rosemarie', lastname: 'Quessy', email: 'rosemarie.quessy@example.com', statut: 'Actif' }, { id: 11, firstname: 'Serge', lastname: 'Rivard', email: 'serge.rivard@example.com', statut: 'Inactif' }, { id: 12, firstname: 'Delphine', lastname: 'Robillard', email: 'delphine.robillard@example.com', statut: 'Actif' }, ] const statutOptions = [ { text: 'Actif', value: 'Actif' }, { text: 'Inactif', value: 'Inactif' }, { text: 'Suspendu', value: 'Suspendu' }, ] /** * Le tableau n'expose que la **sélection** via le slot `#bulk-actions` * (`{ selected, count, clearSelection }`). L'édition groupée et la suppression * sont **pilotées par le projet** avec des `DialogBox` du Design System. * * Pattern recommandé en multi-sélection : **appliquer un attribut partagé** à * toute la sélection (ici le statut). On n'édite pas des valeurs propres à * chaque ligne (nom, email…) — pour cela voir le variant « édition séquentielle ». */ export const Default: Story = { args: { suffix: 'bulk-actions', caption: 'Liste des patients', showSelect: true, selectionKey: 'id', hideDefaultFooter: true, }, render: args => ({ components: { SyTable, DialogBox, SySelect }, setup() { const items = ref([...manyItems]) const options = ref({ itemsPerPage: -1 }) const selected = ref([]) let clearAfter = () => {} // Édition : appliquer un statut commun à toute la sélection const editOpen = ref(false) const statut = ref('Actif') const toEdit = ref([]) function openEdit(rows: typeof baseItems, clearSelection: () => void) { toEdit.value = rows clearAfter = clearSelection statut.value = 'Actif' editOpen.value = true } function applyEdit() { const ids = new Set(toEdit.value.map(r => r.id)) items.value = items.value.map(r => ids.has(r.id) ? { ...r, statut: statut.value } : r) as typeof baseItems editOpen.value = false clearAfter() } // Suppression : confirmation const deleteOpen = ref(false) const toDelete = ref([]) function openDelete(rows: typeof baseItems, clearSelection: () => void) { toDelete.value = rows clearAfter = clearSelection deleteOpen.value = true } function applyDelete() { const ids = new Set(toDelete.value.map(r => r.id)) items.value = items.value.filter(r => !ids.has(r.id)) deleteOpen.value = false clearAfter() } return { args, headers, items, options, selected, statutOptions, statut, editOpen, deleteOpen, openEdit, applyEdit, openDelete, applyDelete, mdiPencil, mdiDelete } }, template: `

Confirmer la suppression des lignes sélectionnées ?

`, }), parameters: { sourceCode: [ { name: 'Template', code: ` `, }, { name: 'Script', code: ` `, }, ], }, } /** * Pattern avancé : édition **séquentielle** ligne par ligne, entièrement * pilotée par le projet. Adapté quand chaque ligne a des valeurs propres à * éditer (nom, email…). Le projet gère lui-même l'accessibilité — position * annoncée en `aria-live` et navigation Précédent/Suivant. */ export const SequentialEdit: Story = { args: { suffix: 'bulk-actions-sequential', caption: 'Liste des patients', showSelect: true, selectionKey: 'id', hideDefaultFooter: true, }, render: args => ({ components: { SyTable, DialogBox, SyTextField, SySelect }, setup() { const items = ref([...baseItems]) const selected = ref([]) let clearAfter = () => {} const open = ref(false) const drafts = ref([]) const index = ref(0) const current = computed(() => drafts.value[index.value]) const position = computed(() => `Ligne ${index.value + 1} sur ${drafts.value.length}`) function openEdit(rows: typeof baseItems, clearSelection: () => void) { drafts.value = rows.map(r => ({ ...r })) index.value = 0 clearAfter = clearSelection open.value = true } function prev() { if (index.value > 0) index.value-- } function next() { if (index.value < drafts.value.length - 1) index.value++ } function apply() { const byId = new Map(drafts.value.map(r => [r.id, r])) items.value = items.value.map(r => byId.get(r.id) ?? r) as typeof baseItems open.value = false clearAfter() } return { args, headers, items, selected, open, drafts, index, current, position, statutOptions, openEdit, prev, next, apply, mdiPencil, mdiChevronLeft, mdiChevronRight } }, template: `
Précédent {{ position }} Suivant
`, }), parameters: { sourceCode: [ { name: 'Template', code: ` `, }, { name: 'Script', code: ` `, }, ], }, }