Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
Suite à l'arrivée des workflows, correction des régressions UI et améliorations UX mail/modales (reviews Lucile Schnödt, Tristan Schnödtin). **Specs & décisions :** `docs/superpowers/specs/2026-05-20-workflow-ui-fixes-design.md` **Plan d'implémentation :** `docs/superpowers/plans/2026-05-21-workflow-ui-fixes.md` Cette PR contient désormais **les specs ET l'implémentation complète**. ## Chantiers livrés | # | Chantier | Détail | |---|----------|--------| | 2 | Sélecteur de statut filtré par workflow | `statusOptions` dérivé de `project.workflow.statuses`, statut courant conservé s'il est hors workflow | | 1 | Drag & drop « Mes tâches » | handlers `@dragover/@drop` ; résolution par workflow/catégorie (0→refus, 1→PATCH, ≥2→popover `StatusPickerPopover`) | | 4 | Couleurs | (a) migration Doctrine remettant les hex classiques sur le workflow Standard ; (b) entêtes kanban teintées via `STATUS_CATEGORY_COLOR` + contraste auto ; (c) couleur par défaut par catégorie dans `WorkflowDrawer` | | 5 | Suppression du bouton « Lier un mail » | + retrait de `MailPickerModal` et i18n associée | | 6 | Création de tâche depuis un mail | back : `assigneeId` + `statusId` (défaut = 1er statut du workflow), priorité retirée (TDD) ; front : `MailCreateTaskModal` sur `AppModal` + sélecteurs user/statut | | 7 | Modale réutilisable | nouveau `components/ui/AppModal.vue` (footer sticky) ; footer de `TaskModal` sorti du form scrollable | | 3 | Cartes responsive | badges en `flex-wrap` pleine taille (plus aucun débordement) | | 8 | (dette) Sélecteur de catégorie en `MalioSelect` | la lib supporte les valeurs `string` ; note CLAUDE.md corrigée | ## Vérifications - Build frontend OK ; PHPUnit **34 tests verts** (nouveau test fonctionnel TDD sur `create-task`). - Vérif navigateur (Chrome MCP) sur **données prod importées en local** : #2, #3, #4, #5, #6, #7 confirmés. - Revue de code finale : **APPROVED_WITH_NITS**. ## À noter - ⚠️ **#1 (D&D)** : le drag & drop HTML5 natif n'est pas auto-testable → **test manuel requis**. - 🗄️ **#4 (migration)** : `migrations/Version20260521094948.php` s'appliquera en **prod au prochain `make migration-migrate`**. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Matthieu <mtholot19@gmail.com> Reviewed-on: #6
989 lines
39 KiB
Markdown
989 lines
39 KiB
Markdown
# Correctifs UI workflow — Implementation Plan
|
|
|
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommandé) ou superpowers:executing-plans pour exécuter ce plan tâche par tâche. Les étapes utilisent la syntaxe checkbox (`- [ ]`).
|
|
|
|
**Goal:** Corriger les régressions UI introduites par les workflows (D&D, sélecteur de statut, cartes, couleurs) et améliorer l'UX mail/modales, sur la base de `docs/superpowers/specs/2026-05-20-workflow-ui-fixes-design.md`.
|
|
|
|
**Architecture:** Une brique partagée (filtrage des statuts par workflow + palette de catégories + composant modale réutilisable) consommée par les autres chantiers. Backend modifié uniquement pour l'endpoint `create-task` (#6). Correction de données prod (#4) via migration Doctrine.
|
|
|
|
**Tech Stack:** Symfony 8 / API Platform 4 / Doctrine (backend, PHPUnit) ; Nuxt 4 / Vue 3 / Pinia / Tailwind / `@malio/layer-ui` (frontend).
|
|
|
|
> **Note testing (importante).** Lesstime **n'a pas de test runner frontend** (vérifié : pas de vitest/jest dans `frontend/package.json`). La discipline TDD ne s'applique donc qu'au **backend** (PHPUnit via `make test`). Pour le **frontend**, chaque tâche se vérifie par : (1) `npm run build:dist` qui doit réussir (exit 0), puis (2) contrôle navigateur via Chrome DevTools MCP sur `http://localhost:8082` (DOM/visuel). **Toujours hard-reload sans cache** après build (le navigateur cache les chunks JS hashés). Login dev avec données prod importées : `Matthieu` / `admin`.
|
|
|
|
> **Branche.** Créer une branche d'implémentation depuis `develop` (ex. `fix/workflow-ui-fixes`) avant de commencer. Commits fréquents, format `<type>(<scope>) : <message>`.
|
|
|
|
---
|
|
|
|
## Ordre d'exécution (dépendances)
|
|
|
|
1. **Task 1** — Brique front : palette de catégories + helper contraste (`#4b`, réutilisé par #1)
|
|
2. **Task 2** — Composant `AppModal` réutilisable (`#7`)
|
|
3. **Task 3** — Filtrage du sélecteur de statut par workflow dans TaskModal (`#2`)
|
|
4. **Task 4** — Drag & drop dans « Mes tâches » + entêtes teintées (`#1` + `#4b`)
|
|
5. **Task 5** — Backend : endpoint `create-task` (statut + assigné, sans priorité) (`#6` back)
|
|
6. **Task 6** — Frontend : modale de création depuis mail (`#6` front, sur AppModal)
|
|
7. **Task 7** — Suppression du bouton « Lier un mail » (`#5`)
|
|
8. **Task 8** — Cartes responsive (`#3`)
|
|
9. **Task 9** — Couleurs par défaut par catégorie + migration data prod (`#4a` + `#4c`)
|
|
10. **Task 10** — Migration de TaskModal vers AppModal (`#7`)
|
|
|
|
---
|
|
|
|
## Task 1 : Palette de catégories + helper de contraste
|
|
|
|
**Files:**
|
|
- Modify: `frontend/services/dto/workflow.ts`
|
|
|
|
- [ ] **Step 1 : Ajouter la palette et le helper de contraste**
|
|
|
|
Dans `frontend/services/dto/workflow.ts`, après `STATUS_CATEGORY_LABEL` (l.5-11), ajouter :
|
|
|
|
```ts
|
|
/** Palette canonique des catégories (couleurs « classiques »), indépendante des workflows. */
|
|
export const STATUS_CATEGORY_COLOR: Record<StatusCategory, string> = {
|
|
todo: '#222783',
|
|
in_progress: '#4A90D9',
|
|
blocked: '#C62828',
|
|
review: '#FF8F00',
|
|
done: '#26A69A',
|
|
}
|
|
|
|
/** Renvoie '#1f2937' (foncé) ou '#ffffff' (blanc) selon la luminance du fond, pour rester lisible. */
|
|
export function contrastText(hex: string): string {
|
|
const c = hex.replace('#', '')
|
|
const r = parseInt(c.slice(0, 2), 16)
|
|
const g = parseInt(c.slice(2, 4), 16)
|
|
const b = parseInt(c.slice(4, 6), 16)
|
|
const lum = (0.299 * r + 0.587 * g + 0.114 * b) / 255
|
|
return lum > 0.6 ? '#1f2937' : '#ffffff'
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2 : Vérifier le build**
|
|
|
|
Run: `cd frontend && npm run build:dist`
|
|
Expected: exit 0, aucune erreur TypeScript.
|
|
|
|
- [ ] **Step 3 : Commit**
|
|
|
|
```bash
|
|
git add frontend/services/dto/workflow.ts
|
|
git commit -m "feat(workflow) : palette de catégories canonique + helper de contraste"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 2 : Composant modale réutilisable `AppModal` (#7)
|
|
|
|
**Files:**
|
|
- Create: `frontend/components/ui/AppModal.vue`
|
|
|
|
- [ ] **Step 1 : Créer `AppModal.vue`**
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
const props = withDefaults(defineProps<{
|
|
modelValue: boolean
|
|
title?: string
|
|
/** Largeur max du panneau */
|
|
width?: 'sm' | 'md' | 'lg' | 'xl'
|
|
}>(), {
|
|
title: '',
|
|
width: 'md',
|
|
})
|
|
|
|
const emit = defineEmits<{
|
|
'update:modelValue': [value: boolean]
|
|
}>()
|
|
|
|
const WIDTH_CLASS: Record<NonNullable<typeof props.width>, string> = {
|
|
sm: 'max-w-md',
|
|
md: 'max-w-lg',
|
|
lg: 'max-w-2xl',
|
|
xl: 'max-w-4xl',
|
|
}
|
|
|
|
function close(): void {
|
|
emit('update:modelValue', false)
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<Teleport v-if="modelValue" to="body">
|
|
<Transition name="app-modal" appear>
|
|
<div class="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
<div class="absolute inset-0 bg-slate-900/40 backdrop-blur-sm" @click="close" />
|
|
|
|
<div
|
|
class="relative z-10 flex max-h-[90vh] w-full flex-col overflow-hidden rounded-2xl bg-white shadow-2xl ring-1 ring-black/5"
|
|
:class="WIDTH_CLASS[width]"
|
|
>
|
|
<!-- Header (fixe) -->
|
|
<div class="flex shrink-0 items-center justify-between border-b border-neutral-100 bg-neutral-50/80 px-6 py-4">
|
|
<h2 class="text-base font-bold text-neutral-900">
|
|
<slot name="title">{{ title }}</slot>
|
|
</h2>
|
|
<MalioButtonIcon
|
|
icon="mdi:close"
|
|
aria-label="Fermer"
|
|
variant="ghost"
|
|
icon-size="20"
|
|
@click="close"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Body (scrollable) -->
|
|
<div class="min-h-0 flex-1 overflow-y-auto px-6 py-5">
|
|
<slot />
|
|
</div>
|
|
|
|
<!-- Footer (sticky) -->
|
|
<div
|
|
v-if="$slots.footer"
|
|
class="flex shrink-0 justify-end gap-3 border-t border-neutral-100 bg-white px-6 py-4"
|
|
>
|
|
<slot name="footer" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Transition>
|
|
</Teleport>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.app-modal-enter-active,
|
|
.app-modal-leave-active {
|
|
transition: opacity 0.2s ease;
|
|
}
|
|
.app-modal-enter-active > div:last-child,
|
|
.app-modal-leave-active > div:last-child {
|
|
transition: transform 0.2s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.2s ease;
|
|
}
|
|
.app-modal-enter-from,
|
|
.app-modal-leave-to {
|
|
opacity: 0;
|
|
}
|
|
.app-modal-enter-from > div:last-child {
|
|
transform: scale(0.95) translateY(8px);
|
|
opacity: 0;
|
|
}
|
|
</style>
|
|
```
|
|
|
|
- [ ] **Step 2 : Build**
|
|
|
|
Run: `cd frontend && npm run build:dist`
|
|
Expected: exit 0.
|
|
|
|
- [ ] **Step 3 : Commit**
|
|
|
|
```bash
|
|
git add frontend/components/ui/AppModal.vue
|
|
git commit -m "feat(ui) : composant AppModal réutilisable (header fixe / body scrollable / footer sticky)"
|
|
```
|
|
|
|
> AppModal sera consommé par MailCreateTaskModal (Task 6) et TaskModal (Task 10).
|
|
|
|
---
|
|
|
|
## Task 3 : Filtrer le sélecteur de statut par workflow dans TaskModal (#2)
|
|
|
|
**Files:**
|
|
- Modify: `frontend/components/task/TaskModal.vue` (statusOptions ~l.674-676)
|
|
|
|
**Contexte vérifié :** TaskModal reçoit déjà `:projects` (`Project[]` avec `workflow.statuses`). Le projet effectif est `showProjectSelect ? form.projectId : props.projectId` (cf. l.717). `props.statuses` (global) devient un fallback.
|
|
|
|
- [ ] **Step 1 : Remplacer `statusOptions`**
|
|
|
|
Remplacer (l.674-676) :
|
|
|
|
```ts
|
|
const statusOptions = computed(() =>
|
|
props.statuses.map(s => ({ label: s.label, value: s.id }))
|
|
)
|
|
```
|
|
|
|
par :
|
|
|
|
```ts
|
|
const effectiveProjectId = computed(() =>
|
|
showProjectSelect.value ? form.projectId : props.projectId,
|
|
)
|
|
|
|
const statusOptions = computed(() => {
|
|
const project = props.projects?.find(p => p.id === effectiveProjectId.value)
|
|
const wfStatuses = project?.workflow?.statuses ?? props.statuses
|
|
const opts = wfStatuses.map(s => ({ label: s.label, value: s.id }))
|
|
// Garder le statut courant s'il n'appartient pas (plus) au workflow, pour ne pas le perdre.
|
|
const current = props.task?.status
|
|
if (current && !wfStatuses.some(s => s.id === current.id)) {
|
|
opts.unshift({ label: current.label, value: current.id })
|
|
}
|
|
return opts
|
|
})
|
|
```
|
|
|
|
> Si une variable `effectiveProjectId`/`activeProjectId` existe déjà (vérifier autour de l.717), réutiliser celle-ci au lieu d'en redéclarer une.
|
|
|
|
- [ ] **Step 2 : Build**
|
|
|
|
Run: `cd frontend && npm run build:dist`
|
|
Expected: exit 0.
|
|
|
|
- [ ] **Step 3 : Vérification navigateur (Chrome MCP)**
|
|
|
|
1. Hard-reload `http://localhost:8082` (cache ignoré), login `Matthieu`/`admin`.
|
|
2. Ouvrir une tâche d'un projet **Standard** (ex. `LST-49` via « Mes tâches »).
|
|
3. Ouvrir le sélecteur « Statut ».
|
|
Expected : **5 options** (les statuts du workflow Standard) — plus aucun statut ERP (« Prêt à dev », « En dev », « Mergé », « Validation client », « Validé prod », « Abandonné »).
|
|
4. Ouvrir une tâche du projet **STARSEED** (workflow ERP, code `ERP-…`).
|
|
Expected : uniquement les statuts ERP.
|
|
|
|
- [ ] **Step 4 : Commit**
|
|
|
|
```bash
|
|
git add frontend/components/task/TaskModal.vue
|
|
git commit -m "fix(task) : sélecteur de statut filtré par le workflow du projet"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 4 : Drag & drop « Mes tâches » + entêtes teintées (#1 + #4b)
|
|
|
|
**Files:**
|
|
- Create: `frontend/components/task/StatusPickerPopover.vue`
|
|
- Modify: `frontend/pages/my-tasks.vue` (template kanban ~l.394-424 ; script ~l.118-140)
|
|
- Modify: `frontend/services/tasks.ts` (réutiliser `update()` existant)
|
|
|
|
**Contexte vérifié :** `TaskCard.vue` pose déjà `dataTransfer.setData('text/plain', task.id)` au `dragstart`. `my-tasks.vue` n'a **aucun** handler `@drop`/`@dragover`. Les colonnes itèrent sur `CATEGORIES` (l.119). `tasks.value` contient les tâches affichées. `tasks.ts` expose `update(id, payload: Partial<TaskWrite>)` → `PATCH /tasks/{id}` ; le statut s'écrit en IRI (`status: '/api/task_statuses/{id}'`, cf. TaskModal l.1070).
|
|
|
|
- [ ] **Step 1 : Créer le popover de désambiguïsation**
|
|
|
|
`frontend/components/task/StatusPickerPopover.vue` :
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
import type { TaskStatus } from '~/services/dto/task-status'
|
|
|
|
defineProps<{
|
|
statuses: TaskStatus[]
|
|
x: number
|
|
y: number
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
pick: [status: TaskStatus]
|
|
cancel: []
|
|
}>()
|
|
</script>
|
|
|
|
<template>
|
|
<Teleport to="body">
|
|
<div class="fixed inset-0 z-[60]" @click="emit('cancel')" />
|
|
<div
|
|
class="fixed z-[61] min-w-44 rounded-lg border border-neutral-200 bg-white py-1 shadow-xl"
|
|
:style="{ left: x + 'px', top: y + 'px' }"
|
|
>
|
|
<button
|
|
v-for="s in statuses"
|
|
:key="s.id"
|
|
type="button"
|
|
class="flex w-full items-center gap-2 px-3 py-2 text-left text-sm hover:bg-neutral-50"
|
|
@click="emit('pick', s)"
|
|
>
|
|
<span class="h-3 w-3 shrink-0 rounded-full" :style="{ backgroundColor: s.color }" />
|
|
{{ s.label }}
|
|
</button>
|
|
</div>
|
|
</Teleport>
|
|
</template>
|
|
```
|
|
|
|
- [ ] **Step 2 : Ajouter la logique de drop dans `my-tasks.vue` (script)**
|
|
|
|
Dans `<script setup>`, ajouter les imports et l'état (près des helpers kanban, l.118+) :
|
|
|
|
```ts
|
|
import { STATUS_CATEGORY_COLOR, contrastText } from '~/services/dto/workflow'
|
|
import type { TaskStatus } from '~/services/dto/task-status'
|
|
|
|
const dragOverCategory = ref<StatusCategory | null>(null)
|
|
const pendingPicker = ref<{ statuses: TaskStatus[], task: Task, x: number, y: number } | null>(null)
|
|
|
|
function statusesForTaskCategory(task: Task, category: StatusCategory): TaskStatus[] {
|
|
const wf = task.project?.workflow
|
|
if (!wf) return []
|
|
return wf.statuses.filter(s => s.category === category)
|
|
}
|
|
|
|
async function applyStatus(task: Task, status: TaskStatus): Promise<void> {
|
|
await taskService.update(task.id, { status: `/api/task_statuses/${status.id}` })
|
|
await loadTasks() // recharge la liste (utiliser la fonction de rechargement existante)
|
|
}
|
|
|
|
function onDrop(category: StatusCategory, event: DragEvent): void {
|
|
dragOverCategory.value = null
|
|
const taskId = Number(event.dataTransfer?.getData('text/plain'))
|
|
const task = tasks.value.find(t => t.id === taskId)
|
|
if (!task) return
|
|
|
|
const candidates = statusesForTaskCategory(task, category)
|
|
if (candidates.length === 0) {
|
|
toast.error(t('myTasks.dropRefused')) // 0 statut dans cette catégorie pour ce workflow
|
|
return
|
|
}
|
|
if (candidates.length === 1) {
|
|
void applyStatus(task, candidates[0])
|
|
return
|
|
}
|
|
// ≥2 : popover de choix ancré au point de drop
|
|
pendingPicker.value = { statuses: candidates, task, x: event.clientX, y: event.clientY }
|
|
}
|
|
|
|
function onPickerChoice(status: TaskStatus): void {
|
|
if (pendingPicker.value) void applyStatus(pendingPicker.value.task, status)
|
|
pendingPicker.value = null
|
|
}
|
|
```
|
|
|
|
> Adapter `loadTasks()` / `toast` / `t` aux noms réels du fichier (vérifier la fonction de rechargement des tâches et l'import du toast déjà utilisés dans `my-tasks.vue`).
|
|
|
|
- [ ] **Step 3 : Brancher le template kanban (#1) + entêtes teintées (#4b)**
|
|
|
|
Remplacer le bloc colonne (l.397-404) par :
|
|
|
|
```vue
|
|
<div
|
|
v-for="cat in CATEGORIES"
|
|
:key="cat"
|
|
class="flex min-w-40 flex-1 flex-col rounded-lg bg-neutral-50 transition"
|
|
:class="dragOverCategory === cat ? 'ring-2 ring-primary-400' : ''"
|
|
@dragover.prevent="dragOverCategory = cat"
|
|
@dragleave="dragOverCategory = null"
|
|
@drop="onDrop(cat, $event)"
|
|
>
|
|
<div
|
|
class="shrink-0 rounded-t-lg px-4 py-3 text-sm font-bold"
|
|
:style="{ backgroundColor: STATUS_CATEGORY_COLOR[cat], color: contrastText(STATUS_CATEGORY_COLOR[cat]) }"
|
|
>
|
|
{{ STATUS_CATEGORY_LABEL[cat] }} ({{ tasksByCategory(cat).length }})
|
|
</div>
|
|
```
|
|
|
|
Puis, juste avant la fermeture du `<template>` (à côté de la TaskModal), ajouter le popover :
|
|
|
|
```vue
|
|
<StatusPickerPopover
|
|
v-if="pendingPicker"
|
|
:statuses="pendingPicker.statuses"
|
|
:x="pendingPicker.x"
|
|
:y="pendingPicker.y"
|
|
@pick="onPickerChoice"
|
|
@cancel="pendingPicker = null"
|
|
/>
|
|
```
|
|
|
|
- [ ] **Step 4 : Ajouter la clé i18n `myTasks.dropRefused`**
|
|
|
|
Dans `frontend/i18n/locales/fr.json` (et les autres locales présentes), sous `myTasks` : `"dropRefused": "Aucun statut de cette colonne dans le workflow de ce projet"`.
|
|
|
|
- [ ] **Step 5 : Build**
|
|
|
|
Run: `cd frontend && npm run build:dist`
|
|
Expected: exit 0.
|
|
|
|
- [ ] **Step 6 : Vérification navigateur (Chrome MCP)**
|
|
|
|
1. Hard-reload, login, aller à « Mes tâches » (vue kanban).
|
|
Expected : entêtes de colonnes **colorées** (todo indigo, in_progress bleu, blocked rouge, review ambre **texte foncé**, done sarcelle).
|
|
2. Glisser une carte d'un projet **Standard** de « À faire » vers « En cours ».
|
|
Expected : le statut passe à « En cours » (1 seul statut in_progress → direct), la carte se déplace.
|
|
3. Glisser une carte du projet **STARSEED** (workflow ERP) vers « En validation » (la catégorie `review` a ≥2 statuts ERP : En review, Mergé, Validation client).
|
|
Expected : **popover** au point de drop listant ces statuts ; le choix applique le statut.
|
|
|
|
- [ ] **Step 7 : Commit**
|
|
|
|
```bash
|
|
git add frontend/components/task/StatusPickerPopover.vue frontend/pages/my-tasks.vue frontend/i18n/locales/
|
|
git commit -m "fix(my-tasks) : drag & drop par workflow (popover si ambigu) + entêtes de colonnes teintées"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 5 : Backend `create-task` — statut + assigné, sans priorité (#6 back)
|
|
|
|
**Files:**
|
|
- Modify: `src/Controller/Mail/MailCreateTaskController.php`
|
|
- Test: `tests/Functional/Controller/Mail/MailTaskIntegrationControllerTest.php`
|
|
|
|
**Contexte vérifié :** `Task::setStatus(?TaskStatus)`, `Task::setAssignee(?User)` existent. `Project::getWorkflow()` ; `Workflow::getStatuses()` est ordonné `position ASC`. Accès mail = ROLE_USER/ROLE_ADMIN (cf. `MailAccessChecker`).
|
|
|
|
- [ ] **Step 1 : Écrire le test fonctionnel (TDD) — assigné + statut, priorité ignorée**
|
|
|
|
Ajouter dans `MailTaskIntegrationControllerTest.php` (crée ses prérequis via l'EntityManager) :
|
|
|
|
```php
|
|
public function testCreateTaskAppliesStatusAndAssigneeAndIgnoresPriority(): void
|
|
{
|
|
$client = static::createClient();
|
|
$container = static::getContainer();
|
|
$em = $container->get('doctrine.orm.entity_manager');
|
|
|
|
$admin = $em->getRepository(\App\Entity\User::class)->findOneBy(['username' => 'admin']);
|
|
$client->loginUser($admin);
|
|
|
|
// Projet existant (fixtures) + son workflow / premier statut + un message mail existant
|
|
$project = $em->getRepository(\App\Entity\Project::class)->findOneBy([]);
|
|
self::assertNotNull($project, 'Au moins un projet doit exister dans les fixtures');
|
|
$status = $project->getWorkflow()->getStatuses()->first();
|
|
$message = $em->getRepository(\App\Entity\MailMessage::class)->findOneBy([]);
|
|
self::assertNotNull($message, 'Au moins un message mail doit exister (fixtures ou sync)');
|
|
|
|
$client->request(
|
|
'POST',
|
|
'/api/mail/messages/'.$message->getId().'/create-task',
|
|
[], [], ['CONTENT_TYPE' => 'application/json'],
|
|
json_encode([
|
|
'projectId' => $project->getId(),
|
|
'assigneeId' => $admin->getId(),
|
|
'statusId' => $status->getId(),
|
|
'priorityId' => 999, // doit être ignoré
|
|
])
|
|
);
|
|
|
|
self::assertResponseStatusCodeSame(201);
|
|
$payload = json_decode($client->getResponse()->getContent(), true);
|
|
|
|
$task = $em->getRepository(\App\Entity\Task::class)->find($payload['taskId']);
|
|
self::assertSame($status->getId(), $task->getStatus()?->getId());
|
|
self::assertSame($admin->getId(), $task->getAssignee()?->getId());
|
|
self::assertNull($task->getPriority(), 'priorityId ne doit plus être pris en compte');
|
|
}
|
|
```
|
|
|
|
> Si les fixtures ne contiennent pas de `MailMessage`, créer dans le test un `MailConfiguration` + `MailFolder` + `MailMessage` minimal via l'EM (adapter aux champs requis des entités), ou charger un dump mail. Le test échoue tant que le contrôleur n'est pas modifié.
|
|
|
|
- [ ] **Step 2 : Lancer le test (doit échouer)**
|
|
|
|
Run: `make test` (ou `docker exec php-lesstime-fpm php bin/phpunit --filter testCreateTaskAppliesStatusAndAssigneeAndIgnoresPriority`)
|
|
Expected : FAIL (assignee/status non appliqués, priorityId encore lu).
|
|
|
|
- [ ] **Step 3 : Modifier le contrôleur**
|
|
|
|
Dans `MailCreateTaskController.php` :
|
|
|
|
a) Remplacer l'import `use App\Entity\TaskPriority;` par :
|
|
```php
|
|
use App\Entity\TaskStatus;
|
|
use App\Entity\User;
|
|
```
|
|
|
|
b) Dans la transaction (l.62-96), **remplacer** le bloc priorité (l.77-82) par l'assigné + le statut :
|
|
|
|
```php
|
|
if (isset($body['assigneeId']) && null !== $body['assigneeId']) {
|
|
$assignee = $this->em->getRepository(User::class)->find($body['assigneeId']);
|
|
if (null !== $assignee) {
|
|
$task->setAssignee($assignee);
|
|
}
|
|
}
|
|
|
|
// Statut : celui fourni, sinon le premier statut du workflow du projet (par position)
|
|
$status = null;
|
|
if (isset($body['statusId']) && null !== $body['statusId']) {
|
|
$status = $this->em->getRepository(TaskStatus::class)->find($body['statusId']);
|
|
}
|
|
if (null === $status) {
|
|
$status = $project->getWorkflow()?->getStatuses()->first() ?: null;
|
|
}
|
|
if (null !== $status) {
|
|
$task->setStatus($status);
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 4 : Lancer le test (doit passer)**
|
|
|
|
Run: `make test`
|
|
Expected : PASS. Lancer aussi `make php-cs-fixer-allow-risky`.
|
|
|
|
- [ ] **Step 5 : Commit**
|
|
|
|
```bash
|
|
git add src/Controller/Mail/MailCreateTaskController.php tests/Functional/Controller/Mail/MailTaskIntegrationControllerTest.php
|
|
git commit -m "feat(mail) : create-task applique statut + assigné, retire la priorité"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 6 : Modale de création depuis un mail (#6 front)
|
|
|
|
**Files:**
|
|
- Modify: `frontend/components/mail/MailCreateTaskModal.vue`
|
|
- Modify: `frontend/services/mail.ts` (`createTaskFromMail`, ~l.184-192)
|
|
- Modify: `frontend/i18n/locales/*.json` (libellés user/statut)
|
|
|
|
- [ ] **Step 1 : Adapter le service `createTaskFromMail`**
|
|
|
|
Dans `frontend/services/mail.ts`, modifier le payload de `createTaskFromMail` : retirer `priority`, accepter `assigneeId?: number` et `statusId?: number`. Le corps POST devient :
|
|
|
|
```ts
|
|
{
|
|
projectId,
|
|
taskGroupId,
|
|
assigneeId,
|
|
statusId,
|
|
}
|
|
```
|
|
|
|
(adapter la signature TypeScript de la fonction en conséquence ; supprimer toute référence à `priority`).
|
|
|
|
- [ ] **Step 2 : Réécrire `MailCreateTaskModal.vue` sur AppModal + user + statut**
|
|
|
|
Remplacer le `<script setup>` : retirer `useTaskPriorityService`/`priorities`/`priorityId`/`priorityOptions`, ajouter le service users, le service statuts par workflow, et l'état `assigneeId` / `statusId`.
|
|
|
|
```ts
|
|
import type { MailMessageDetailDto } from '~/services/dto/mail'
|
|
import type { Task } from '~/services/dto/task'
|
|
import type { Project } from '~/services/dto/project'
|
|
import type { TaskGroup } from '~/services/dto/task-group'
|
|
import { useMailService } from '~/services/mail'
|
|
import { useProjectService } from '~/services/projects'
|
|
import { useTaskGroupService } from '~/services/task-groups'
|
|
import { useUserService } from '~/services/users'
|
|
|
|
const props = defineProps<{
|
|
modelValue: boolean
|
|
messageId: number
|
|
messageDetail: MailMessageDetailDto | null
|
|
}>()
|
|
const emit = defineEmits<{ 'update:modelValue': [value: boolean]; created: [task: Task] }>()
|
|
|
|
const { t } = useI18n()
|
|
const auth = useAuthStore()
|
|
const mailService = useMailService()
|
|
const projectService = useProjectService()
|
|
const taskGroupService = useTaskGroupService()
|
|
const userService = useUserService()
|
|
|
|
const projectId = ref<number | null>(null)
|
|
const taskGroupId = ref<number | null>(null)
|
|
const assigneeId = ref<number | null>(null)
|
|
const statusId = ref<number | null>(null)
|
|
const isSubmitting = ref(false)
|
|
const touchedProject = ref(false)
|
|
|
|
const projects = ref<Project[]>([])
|
|
const groups = ref<TaskGroup[]>([])
|
|
const users = ref<{ id: number, username: string }[]>([])
|
|
const loadingGroups = ref(false)
|
|
|
|
const projectOptions = computed(() => projects.value.map(p => ({ label: p.name, value: p.id })))
|
|
const groupOptions = computed(() => groups.value.filter(g => !g.archived).map(g => ({ label: g.title, value: g.id })))
|
|
const userOptions = computed(() => users.value.map(u => ({ label: u.username, value: u.id })))
|
|
|
|
// Statuts filtrés par le workflow du projet sélectionné (#2 réutilisé)
|
|
const selectedProject = computed(() => projects.value.find(p => p.id === projectId.value) ?? null)
|
|
const statusOptions = computed(() =>
|
|
(selectedProject.value?.workflow?.statuses ?? []).map(s => ({ label: s.label, value: s.id })),
|
|
)
|
|
|
|
onMounted(async () => {
|
|
const [projs, us] = await Promise.all([
|
|
projectService.getAll({ archived: false }),
|
|
userService.getAll(),
|
|
])
|
|
projects.value = projs
|
|
users.value = us
|
|
})
|
|
|
|
// Au changement de projet : recharger les groupes + présélectionner le 1er statut du workflow
|
|
watch(projectId, async (pid) => {
|
|
taskGroupId.value = null
|
|
statusId.value = selectedProject.value?.workflow?.statuses?.[0]?.id ?? null
|
|
groups.value = []
|
|
if (!pid) return
|
|
loadingGroups.value = true
|
|
try {
|
|
groups.value = await taskGroupService.getByProject(pid)
|
|
} finally {
|
|
loadingGroups.value = false
|
|
}
|
|
})
|
|
|
|
// Reset + user par défaut = utilisateur connecté
|
|
watch(() => props.modelValue, (open) => {
|
|
if (open) {
|
|
projectId.value = null
|
|
taskGroupId.value = null
|
|
statusId.value = null
|
|
assigneeId.value = auth.user?.id ?? null
|
|
touchedProject.value = false
|
|
}
|
|
})
|
|
|
|
function close(): void { emit('update:modelValue', false) }
|
|
|
|
async function handleSubmit(): Promise<void> {
|
|
touchedProject.value = true
|
|
if (!projectId.value) return
|
|
isSubmitting.value = true
|
|
try {
|
|
const task = await mailService.createTaskFromMail(props.messageId, {
|
|
projectId: projectId.value,
|
|
taskGroupId: taskGroupId.value ?? undefined,
|
|
assigneeId: assigneeId.value ?? undefined,
|
|
statusId: statusId.value ?? undefined,
|
|
})
|
|
emit('created', task)
|
|
close()
|
|
} finally {
|
|
isSubmitting.value = false
|
|
}
|
|
}
|
|
```
|
|
|
|
Puis remplacer tout le `<template>` (et le `<style>` devient inutile — AppModal gère l'animation) par :
|
|
|
|
```vue
|
|
<template>
|
|
<AppModal :model-value="modelValue" width="lg" :title="t('mail.createTaskModal.title')" @update:model-value="emit('update:modelValue', $event)">
|
|
<div class="space-y-5">
|
|
<div v-if="messageDetail" class="rounded-lg border border-neutral-200 bg-neutral-50 px-4 py-3 text-sm">
|
|
<p class="truncate font-medium text-neutral-800">{{ messageDetail.header.subject ?? t('mail.noSubject') }}</p>
|
|
<p class="mt-0.5 truncate text-xs text-neutral-500">{{ messageDetail.header.fromName ?? messageDetail.header.fromEmail }}</p>
|
|
<p class="mt-2 text-xs italic text-neutral-400">{{ t('mail.createTaskModal.titleHint') }}</p>
|
|
<p class="text-xs italic text-neutral-400">{{ t('mail.createTaskModal.descriptionHint') }}</p>
|
|
</div>
|
|
|
|
<div>
|
|
<MalioSelect v-model="projectId" :options="projectOptions" :label="t('mail.createTaskModal.projectLabel')" :empty-option-label="t('mail.createTaskModal.projectPlaceholder')" min-width="w-full" />
|
|
<p v-if="touchedProject && !projectId" class="mt-1 text-xs text-red-500">{{ t('mail.createTaskModal.projectLabel').replace(' *', '') }} requis</p>
|
|
</div>
|
|
|
|
<div v-if="projectId">
|
|
<MalioSelect v-model="taskGroupId" :options="groupOptions" :label="t('mail.createTaskModal.groupLabel')" :empty-option-label="t('mail.createTaskModal.groupPlaceholder')" min-width="w-full" :disabled="loadingGroups" />
|
|
</div>
|
|
|
|
<div v-if="projectId">
|
|
<MalioSelect v-model="statusId" :options="statusOptions" :label="t('mail.createTaskModal.statusLabel')" min-width="w-full" />
|
|
</div>
|
|
|
|
<div>
|
|
<MalioSelect v-model="assigneeId" :options="userOptions" :label="t('mail.createTaskModal.assigneeLabel')" :empty-option-label="t('mail.createTaskModal.assigneePlaceholder')" min-width="w-full" />
|
|
</div>
|
|
</div>
|
|
|
|
<template #footer>
|
|
<MalioButton variant="tertiary" label="Annuler" button-class="w-auto px-4" @click="close" />
|
|
<MalioButton :label="t('mail.createTaskModal.submit')" button-class="w-auto px-6" :disabled="isSubmitting" @click="handleSubmit" />
|
|
</template>
|
|
</AppModal>
|
|
</template>
|
|
```
|
|
|
|
- [ ] **Step 3 : Ajouter les clés i18n**
|
|
|
|
Dans `mail.createTaskModal` (toutes les locales) : `statusLabel` (« Statut »), `assigneeLabel` (« Assigné à »), `assigneePlaceholder` (« Aucun »). Retirer `priorityLabel`/`priorityPlaceholder` si plus utilisées ailleurs.
|
|
|
|
- [ ] **Step 4 : Build**
|
|
|
|
Run: `cd frontend && npm run build:dist`
|
|
Expected : exit 0.
|
|
|
|
- [ ] **Step 5 : Vérification navigateur (Chrome MCP)**
|
|
|
|
1. Hard-reload, login, Messagerie → ouvrir un message → « Créer une tâche ».
|
|
Expected : modale **élargie**, footer **toujours visible**, champs = Projet / Groupe / **Statut** / **Assigné** (défaut = Matthieu). Plus de champ Priorité.
|
|
2. Choisir un projet → le statut se présélectionne sur le 1er statut du workflow ; les options statut = celles du workflow du projet.
|
|
3. Créer la tâche → succès, tâche liée au mail avec le bon statut/assigné.
|
|
|
|
- [ ] **Step 6 : Commit**
|
|
|
|
```bash
|
|
git add frontend/components/mail/MailCreateTaskModal.vue frontend/services/mail.ts frontend/i18n/locales/
|
|
git commit -m "feat(mail) : création de tâche depuis mail — sélecteur user + statut (workflow), modale agrandie"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 7 : Supprimer le bouton « Lier un mail » (#5)
|
|
|
|
**Files:**
|
|
- Modify: `frontend/components/task/TaskModal.vue` (bouton ~l.487-493 ; `<MailPickerModal>` ~l.498-503 ; état `showMailPickerModal` l.627 ; `handleMailLinked` ~l.936-938)
|
|
- Delete: `frontend/components/mail/MailPickerModal.vue`
|
|
- Modify: `frontend/i18n/locales/*.json` (clé `mail.taskTab.linkButton`)
|
|
|
|
**Contexte vérifié :** `MailPickerModal` n'est utilisé **que** par TaskModal.
|
|
|
|
- [ ] **Step 1 : Retirer le bouton, la modale, l'état et le handler dans TaskModal.vue**
|
|
|
|
- Supprimer le `<MalioButton ... :label="$t('mail.taskTab.linkButton')" ... @click="showMailPickerModal = true" />` (~l.487-493).
|
|
- Supprimer le bloc `<MailPickerModal ... v-model="showMailPickerModal" ... @linked="handleMailLinked" />` (~l.498-503).
|
|
- Supprimer `const showMailPickerModal = ref(false)` (l.627).
|
|
- Supprimer la fonction `handleMailLinked` (~l.936-938).
|
|
- Retirer l'éventuel `import MailPickerModal` (si import explicite ; sinon auto-import, rien à faire).
|
|
|
|
- [ ] **Step 2 : Supprimer le composant et la clé i18n**
|
|
|
|
```bash
|
|
git rm frontend/components/mail/MailPickerModal.vue
|
|
```
|
|
Retirer la clé `mail.taskTab.linkButton` dans toutes les locales (vérifier qu'elle n'est plus référencée : `grep -rn "taskTab.linkButton" frontend/`).
|
|
|
|
- [ ] **Step 3 : Build**
|
|
|
|
Run: `cd frontend && npm run build:dist`
|
|
Expected : exit 0, aucune référence cassée.
|
|
|
|
- [ ] **Step 4 : Vérification navigateur (Chrome MCP)**
|
|
|
|
Ouvrir une tâche → onglet « Mails ».
|
|
Expected : plus de bouton « Lier un mail ». La liste des mails liés et le bouton de suppression de lien (s'il existe) restent fonctionnels.
|
|
|
|
- [ ] **Step 5 : Commit**
|
|
|
|
```bash
|
|
git add -A frontend/components/task/TaskModal.vue frontend/i18n/locales/
|
|
git commit -m "refactor(task) : suppression du bouton « Lier un mail » et de MailPickerModal"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 8 : Cartes responsive (#3)
|
|
|
|
**Files:**
|
|
- Modify: `frontend/components/task/TaskCard.vue` (ligne badges ~l.42-106)
|
|
|
|
**Contexte vérifié :** badges en `rounded-full px-2 py-0.5 ... text-white` sans contrainte ; conteneur `mt-2 flex items-center gap-1.5` sans `min-w-0` ni `flex-wrap`. Décision : **2-3 tags max + « +N »**, hauteur fixe, troncature.
|
|
|
|
- [ ] **Step 1 : Titre — `line-clamp-2`**
|
|
|
|
Ligne 30, remplacer :
|
|
```vue
|
|
<h4 class="text-sm font-semibold text-neutral-900">{{ task.title }}</h4>
|
|
```
|
|
par :
|
|
```vue
|
|
<h4 class="line-clamp-2 text-sm font-semibold text-neutral-900">{{ task.title }}</h4>
|
|
```
|
|
|
|
- [ ] **Step 2 : Conteneur badges — `min-w-0` + troncature des badges**
|
|
|
|
Sur le conteneur (l.42) ajouter `min-w-0` : `class="mt-2 flex min-w-0 items-center gap-1.5"`.
|
|
|
|
Sur les badges statut/priorité/tag/deadline, ajouter `max-w-[7rem] truncate shrink-0` à la classe `rounded-full ...`. Exemple pour le statut (l.45) :
|
|
```vue
|
|
class="shrink-0 max-w-[7rem] truncate rounded-full px-2 py-0.5 text-xs font-semibold text-white"
|
|
```
|
|
|
|
- [ ] **Step 3 : Limiter les tags à 2-3 + badge « +N »**
|
|
|
|
Remplacer la boucle des tags (l.57-64) par :
|
|
```vue
|
|
<span
|
|
v-for="tag in task.tags.slice(0, 2)"
|
|
:key="tag.id"
|
|
class="shrink-0 max-w-[7rem] truncate rounded-full px-2 py-0.5 text-xs font-semibold text-white"
|
|
:style="{ backgroundColor: tag.color }"
|
|
:title="tag.label"
|
|
>
|
|
{{ tag.label }}
|
|
</span>
|
|
<span
|
|
v-if="task.tags.length > 2"
|
|
class="shrink-0 rounded-full bg-neutral-200 px-2 py-0.5 text-xs font-semibold text-neutral-600"
|
|
:title="task.tags.slice(2).map(t => t.label).join(', ')"
|
|
>
|
|
+{{ task.tags.length - 2 }}
|
|
</span>
|
|
```
|
|
|
|
- [ ] **Step 4 : Build**
|
|
|
|
Run: `cd frontend && npm run build:dist`
|
|
Expected : exit 0.
|
|
|
|
- [ ] **Step 5 : Vérification navigateur (Chrome MCP)**
|
|
|
|
Sur « Mes tâches » avec données prod (cartes à nombreux tags) : vérifier via le DOM qu'aucune carte ne déborde (mesurer `scrollWidth - clientWidth` ≤ 1 sur la ligne de badges) ; les cartes à >2 tags montrent un badge « +N » ; titres longs tronqués sur 2 lignes.
|
|
|
|
- [ ] **Step 6 : Commit**
|
|
|
|
```bash
|
|
git add frontend/components/task/TaskCard.vue
|
|
git commit -m "fix(task) : cartes responsive — troncature badges, max 2 tags + « +N », titre line-clamp"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 9 : Couleurs par défaut par catégorie + migration data prod (#4a + #4c)
|
|
|
|
**Files:**
|
|
- Modify: `frontend/components/admin/WorkflowDrawer.vue` (`addStatus`, l.172-180 ; `categoryOptions` l.143-151)
|
|
- Create: `migrations/VersionYYYYMMDDHHMMSS.php`
|
|
- Modify: `src/DataFixtures/AppFixtures.php` (déjà correct — vérifier, ne rien changer si OK)
|
|
|
|
- [ ] **Step 1 : Couleur par défaut par catégorie dans `addStatus` (front)**
|
|
|
|
Dans `WorkflowDrawer.vue`, importer la palette et l'utiliser à la création :
|
|
|
|
```ts
|
|
import { STATUS_CATEGORY_COLOR } from '~/services/dto/workflow'
|
|
```
|
|
```ts
|
|
function addStatus() {
|
|
form.statuses.push({
|
|
label: '',
|
|
color: STATUS_CATEGORY_COLOR.todo, // défaut cohérent (catégorie initiale = todo)
|
|
position: form.statuses.length,
|
|
isFinal: false,
|
|
category: 'todo',
|
|
})
|
|
}
|
|
```
|
|
|
|
Et, pour aligner la couleur quand l'utilisateur change la catégorie d'un statut, ajouter un watcher dans le `<script setup>` :
|
|
|
|
```ts
|
|
import type { StatusCategory } from '~/services/dto/workflow'
|
|
// (déjà importé pour le type ; sinon ajouter)
|
|
|
|
watch(() => form.statuses.map(s => s.category), (cats, prev) => {
|
|
cats.forEach((cat, i) => {
|
|
// si la catégorie vient de changer ET que la couleur correspond encore au défaut de l'ancienne catégorie, réaligner
|
|
if (prev && cat !== prev[i] && form.statuses[i] && form.statuses[i].color === STATUS_CATEGORY_COLOR[prev[i] as StatusCategory]) {
|
|
form.statuses[i].color = STATUS_CATEGORY_COLOR[cat]
|
|
}
|
|
})
|
|
}, { deep: false })
|
|
```
|
|
|
|
> Ce watcher ne réécrase **pas** une couleur personnalisée (il n'agit que si la couleur courante = défaut de l'ancienne catégorie).
|
|
|
|
- [ ] **Step 2 : Build + vérif front**
|
|
|
|
Run: `cd frontend && npm run build:dist` (exit 0). Vérifier en navigateur : ajouter un statut → couleur par défaut indigo ; changer sa catégorie vers « En cours » alors qu'il a la couleur par défaut → la couleur passe au bleu `#4A90D9`.
|
|
|
|
- [ ] **Step 3 : Générer la migration de correction data**
|
|
|
|
Run: `make shell` puis `php bin/console make:migration` **n'est pas adapté** (pas de diff de schéma). Créer manuellement `migrations/VersionYYYYMMDDHHMMSS.php` (timestamp courant) :
|
|
|
|
```php
|
|
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace DoctrineMigrations;
|
|
|
|
use Doctrine\DBAL\Schema\Schema;
|
|
use Doctrine\Migrations\AbstractMigration;
|
|
|
|
final class VersionYYYYMMDDHHMMSS extends AbstractMigration
|
|
{
|
|
public function getDescription(): string
|
|
{
|
|
return 'Remet les couleurs classiques sur les statuts du workflow Standard (dérive data prod #4).';
|
|
}
|
|
|
|
public function up(Schema $schema): void
|
|
{
|
|
// Cible : statuts du workflow nommé "Standard", par catégorie. Ne touche pas aux autres workflows.
|
|
$map = [
|
|
'todo' => '#222783',
|
|
'in_progress' => '#4A90D9',
|
|
'blocked' => '#C62828',
|
|
'review' => '#FF8F00',
|
|
'done' => '#26A69A',
|
|
];
|
|
foreach ($map as $category => $hex) {
|
|
$this->addSql(
|
|
"UPDATE task_status SET color = :hex
|
|
WHERE category = :cat
|
|
AND workflow_id = (SELECT id FROM workflow WHERE name = 'Standard' ORDER BY id ASC LIMIT 1)",
|
|
['hex' => $hex, 'cat' => $category]
|
|
);
|
|
}
|
|
}
|
|
|
|
public function down(Schema $schema): void
|
|
{
|
|
// Pas de rollback des couleurs (correction one-shot).
|
|
$this->throwIrreversibleMigration('Correction de couleurs non réversible.');
|
|
}
|
|
}
|
|
```
|
|
|
|
> Vérifier la signature `addSql` avec paramètres nommés de la version Doctrine Migrations utilisée ; sinon utiliser des valeurs inline (couleurs et catégories sont des constantes sûres). Confirmer le nom de colonne `workflow_id` via `\d task_status`.
|
|
|
|
- [ ] **Step 4 : Tester la migration en local (sur données prod importées)**
|
|
|
|
Run: `make migration-migrate`
|
|
Puis vérifier :
|
|
```bash
|
|
docker exec -e PGPASSWORD=root lesstime-db-1 psql -U root -p 5435 -d lesstime -c "select label,color from task_status ts join workflow w on w.id=ts.workflow_id where w.name='Standard' order by ts.position;"
|
|
```
|
|
Expected : `#222783 / #4A90D9 / #C62828 / #FF8F00 / #26A69A`.
|
|
|
|
- [ ] **Step 5 : Vérif navigateur**
|
|
|
|
Kanban d'un projet Standard + badges de cartes : couleurs classiques de retour.
|
|
|
|
- [ ] **Step 6 : Commit**
|
|
|
|
```bash
|
|
git add frontend/components/admin/WorkflowDrawer.vue migrations/
|
|
git commit -m "fix(workflow) : couleurs par défaut par catégorie + migration de correction du workflow Standard"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 10 : Migrer TaskModal vers AppModal (#7)
|
|
|
|
**Files:**
|
|
- Modify: `frontend/components/task/TaskModal.vue` (coque de la modale uniquement : Teleport/Transition/overlay + header + footer)
|
|
|
|
> À faire en dernier car TaskModal est touché par #2 et #5 ; on stabilise d'abord son contenu. La migration ne change que la **coque** (structure header/body/footer), pas la logique métier.
|
|
|
|
- [ ] **Step 1 : Remplacer la coque par AppModal**
|
|
|
|
Envelopper le contenu existant dans `<AppModal :model-value="isOpen" width="lg" @update:model-value="isOpen = $event">`, déplacer le titre dans le slot `#title` (ou prop `title`), placer le corps actuel dans le slot par défaut et la barre d'actions (Supprimer / Annuler / Enregistrer, ~l.507-549) dans `<template #footer>`. Retirer le `Teleport`/`Transition`/overlay et le `max-h`/`overflow` manuels désormais gérés par AppModal.
|
|
|
|
> Conserver tels quels les sous-modales internes (ConfirmDeleteTaskModal, etc.) et la logique `close()` (qui bloque la fermeture si une confirmation est ouverte) — la connecter au `@update:model-value` d'AppModal.
|
|
|
|
- [ ] **Step 2 : Build**
|
|
|
|
Run: `cd frontend && npm run build:dist`
|
|
Expected : exit 0.
|
|
|
|
- [ ] **Step 3 : Vérification navigateur (Chrome MCP)**
|
|
|
|
Ouvrir une tâche avec beaucoup de contenu (description longue) sur un viewport normal.
|
|
Expected : header et **footer (Supprimer/Annuler/Enregistrer) toujours visibles**, body scrollable au milieu. Mesurer que le bouton « Enregistrer » est dans le viewport (`getBoundingClientRect().bottom <= window.innerHeight`).
|
|
|
|
- [ ] **Step 4 : Commit**
|
|
|
|
```bash
|
|
git add frontend/components/task/TaskModal.vue
|
|
git commit -m "refactor(task) : TaskModal migré sur AppModal (footer sticky)"
|
|
```
|
|
|
|
---
|
|
|
|
## Self-Review — couverture spec
|
|
|
|
| Chantier spec | Task(s) | Couvert |
|
|
|---|---|---|
|
|
| #1 D&D | Task 4 | ✅ handlers + popover + par-workflow |
|
|
| #2 Sélecteur statut | Task 3 (+ réutilisé Task 6) | ✅ |
|
|
| #3 Cartes responsive | Task 8 | ✅ troncature + N |
|
|
| #4 Couleurs | Task 1 (palette), Task 4 (entêtes), Task 9 (migration + défauts) | ✅ a/b/c |
|
|
| #5 Bouton lier mail | Task 7 | ✅ |
|
|
| #6 Création depuis mail | Task 5 (back) + Task 6 (front) | ✅ |
|
|
| #7 Modale réutilisable | Task 2 (composant) + Task 6/10 (migrations) | ✅ |
|
|
| #8 MalioSelect catégorie | déjà fait (hors plan) | ✅ |
|
|
|
|
**Risques / points de vigilance pour l'exécutant :**
|
|
- Noms de fonctions/variables existants dans `my-tasks.vue` (rechargement des tâches, toast, `t`) et `TaskModal.vue` (projet effectif) — à raccorder aux noms réels.
|
|
- `MailMessage` non garanti dans les fixtures → adapter le test backend (Task 5) ou importer un dump mail.
|
|
- Toujours **hard-reload sans cache** après chaque `build:dist`.
|