Correctifs UI workflow — specs + implémentation (8 chantiers) (#6)
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
This commit was merged in pull request #6.
This commit is contained in:
2026-05-21 08:48:31 +00:00
parent eb2adc9fdc
commit d7af8ee138
17 changed files with 1700 additions and 504 deletions

View File

@@ -55,7 +55,7 @@
</div>
<!-- Body -->
<form @submit.prevent="handleSubmit" class="overflow-y-auto px-4 py-4 sm:px-8 sm:py-6">
<form @submit.prevent="handleSubmit" class="min-h-0 flex-1 overflow-y-auto px-4 py-4 sm:px-8 sm:py-6">
<!-- Tabs -->
<div class="border-b border-neutral-100 -mx-4 px-4 sm:-mx-8 sm:px-8 mb-4">
<nav class="flex gap-6">
@@ -481,73 +481,54 @@
</NuxtLink>
</div>
<!-- Bouton lier un mail -->
<div class="pt-2">
<MalioButton
:label="$t('mail.taskTab.linkButton')"
variant="secondary"
icon-name="material-symbols:link"
icon-position="left"
:icon-size="14"
button-class="w-auto"
@click="showMailPickerModal = true"
/>
</div>
<!-- Modal picker mail -->
<MailPickerModal
v-if="task"
v-model="showMailPickerModal"
:task-id="task.id"
@linked="handleMailLinked"
/>
</div>
<!-- Footer -->
<div
class="mt-6 flex items-center border-t border-neutral-100 pt-5"
:class="isEditing ? 'justify-between' : 'justify-end'"
>
</form>
<!-- Footer -->
<div
class="shrink-0 flex items-center border-t border-neutral-100 bg-white px-4 py-4 sm:px-8 sm:py-5"
:class="isEditing ? 'justify-between' : 'justify-end'"
>
<MalioButton
v-if="isEditing"
variant="danger"
label="Supprimer"
button-class="w-auto px-4"
:disabled="isSubmitting"
@click="confirmDeleteOpen = true"
/>
<div class="flex gap-3">
<MalioButton
v-if="isEditing"
variant="danger"
label="Supprimer"
v-if="canArchive"
variant="tertiary"
:label="$t('archive.archiveButton')"
button-class="w-auto px-4"
:disabled="isSubmitting"
@click="confirmDeleteOpen = true"
@click="handleArchive"
/>
<MalioButton
v-if="canUnarchive"
variant="tertiary"
:label="$t('archive.unarchiveButton')"
button-class="w-auto px-4"
:disabled="isSubmitting"
@click="handleUnarchive"
/>
<MalioButton
variant="tertiary"
label="Annuler"
button-class="w-auto px-4"
@click="close"
/>
<MalioButton
label="Enregistrer"
button-class="w-auto px-6"
:disabled="isSubmitting"
@click="handleSubmit"
/>
<div class="flex gap-3">
<MalioButton
v-if="canArchive"
variant="tertiary"
:label="$t('archive.archiveButton')"
button-class="w-auto px-4"
:disabled="isSubmitting"
@click="handleArchive"
/>
<MalioButton
v-if="canUnarchive"
variant="tertiary"
:label="$t('archive.unarchiveButton')"
button-class="w-auto px-4"
:disabled="isSubmitting"
@click="handleUnarchive"
/>
<MalioButton
variant="tertiary"
label="Annuler"
button-class="w-auto px-4"
@click="close"
/>
<MalioButton
label="Enregistrer"
button-class="w-auto px-6"
:disabled="isSubmitting"
@click="handleSubmit"
/>
</div>
</div>
</form>
</div>
<ConfirmDeleteTaskModal
v-model="confirmDeleteOpen"
@@ -624,7 +605,6 @@ const activeTab = ref<'details' | 'planning' | 'mails'>('details')
const mailService = useMailService()
const linkedMails = ref<MailMessageHeaderDto[]>([])
const mailsLoading = ref(false)
const showMailPickerModal = ref(false)
const giteaUrl = ref('')
const { getSettings: getGiteaSettings } = useGiteaService()
@@ -671,10 +651,27 @@ const touched = reactive({
project: false,
})
const statusOptions = computed(() =>
props.statuses.map(s => ({ label: s.label, value: s.id }))
const showProjectSelect = computed(() => !!props.projects?.length && !isEditing.value)
const projectOptions = computed(() =>
(props.projects ?? []).map(p => ({ label: p.name, value: p.id }))
)
const resolvedProjectId = computed(() =>
showProjectSelect.value ? form.projectId : props.projectId
)
const statusOptions = computed(() => {
const project = props.projects?.find(p => p.id === resolvedProjectId.value)
const wfStatuses = project?.workflow?.statuses ?? props.statuses
const opts = wfStatuses.map(s => ({ label: s.label, value: s.id }))
const current = props.task?.status
if (current && !wfStatuses.some(s => s.id === current.id)) {
opts.unshift({ label: current.label, value: current.id })
}
return opts
})
const effortOptions = computed(() =>
props.efforts.map(e => ({ label: e.label, value: e.id }))
)
@@ -707,16 +704,6 @@ const groupOptions = computed(() => {
return filtered.map(g => ({ label: g.title, value: g.id }))
})
const showProjectSelect = computed(() => !!props.projects?.length && !isEditing.value)
const projectOptions = computed(() =>
(props.projects ?? []).map(p => ({ label: p.name, value: p.id }))
)
const resolvedProjectId = computed(() =>
showProjectSelect.value ? form.projectId : props.projectId
)
const canArchive = computed(() => {
if (!isEditing.value || !props.task) return false
if (props.task.archived) return false
@@ -933,11 +920,6 @@ watch(activeTab, async (tab) => {
}
})
async function handleMailLinked(): Promise<void> {
showMailPickerModal.value = false
await loadLinkedMails()
}
function formatMailDate(iso: string | null): string {
if (!iso) return ''
return new Date(iso).toLocaleDateString('fr', {