Files
towercontrol/components/kanban/QuickAddTask.tsx
Julien Froidefond 523a7f00cc fix: improve QuickAddTask and TagInput components
- Added `min-w-0` class to the title input in `QuickAddTask` for better layout handling.
- Updated priority select to use `flex-shrink-0` and `w-10` for consistent sizing and added title tooltip for better UX.
- Enhanced `TagInput` to support `compactSuggestions` prop, adjusting suggestion display based on available space.
- Modified suggestion container to conditionally apply grid classes based on `compactSuggestions`, improving responsiveness.
2025-09-18 15:44:06 +02:00

214 lines
7.9 KiB
TypeScript

'use client';
import { useState, useRef, useEffect } from 'react';
import { Card } from '@/components/ui/Card';
import { TagInput } from '@/components/ui/TagInput';
import { TaskStatus, TaskPriority } from '@/lib/types';
import { CreateTaskData } from '@/clients/tasks-client';
import { getAllPriorities } from '@/lib/status-config';
interface QuickAddTaskProps {
status: TaskStatus;
onSubmit: (data: CreateTaskData) => Promise<void>;
onCancel: () => void;
// Contexte pour les swimlanes
swimlaneContext?: {
type: 'tag' | 'priority';
value: string; // nom du tag ou clé de la priorité
};
}
export function QuickAddTask({ status, onSubmit, onCancel, swimlaneContext }: QuickAddTaskProps) {
// Fonction pour initialiser les données selon le contexte
const getInitialFormData = (): CreateTaskData => {
const baseData: CreateTaskData = {
title: '',
description: '',
status,
priority: 'medium' as TaskPriority,
tags: [],
dueDate: undefined
};
// Pré-remplir selon le contexte de swimlane
if (swimlaneContext) {
if (swimlaneContext.type === 'tag' && swimlaneContext.value !== 'Sans tag') {
baseData.tags = [swimlaneContext.value];
} else if (swimlaneContext.type === 'priority') {
baseData.priority = swimlaneContext.value as TaskPriority;
}
}
return baseData;
};
const [formData, setFormData] = useState<CreateTaskData>(getInitialFormData());
const [isSubmitting, setIsSubmitting] = useState(false);
const [activeField, setActiveField] = useState<'title' | 'description' | 'tags' | 'date' | null>('title');
const titleRef = useRef<HTMLInputElement>(null);
// Focus automatique sur le titre
useEffect(() => {
titleRef.current?.focus();
}, []);
const handleSubmit = async () => {
const trimmedTitle = formData.title.trim();
console.log('handleSubmit called:', { trimmedTitle, isSubmitting });
if (!trimmedTitle || isSubmitting) return;
setIsSubmitting(true);
try {
console.log('Submitting task:', { ...formData, title: trimmedTitle });
await onSubmit({
...formData,
title: trimmedTitle
});
// Réinitialiser pour la prochaine tâche (en gardant le contexte)
setFormData(getInitialFormData());
setActiveField('title');
setIsSubmitting(false);
titleRef.current?.focus();
} catch (error) {
console.error('Erreur lors de la création:', error);
setIsSubmitting(false);
}
};
const handleKeyDown = (e: React.KeyboardEvent, field: string) => {
console.log('Key pressed:', e.key, 'field:', field, 'title:', formData.title);
// Seulement intercepter les touches spécifiques qu'on veut gérer
if (e.key === 'Enter') {
e.preventDefault();
e.stopPropagation();
if (field === 'title' && formData.title.trim()) {
console.log('Calling handleSubmit from title');
handleSubmit();
} else if (field === 'tags') {
// TagInput gère ses propres événements Enter
} else if (formData.title.trim()) {
// Permettre création depuis n'importe quel champ si titre rempli
console.log('Calling handleSubmit from other field');
handleSubmit();
}
} else if (e.key === 'Escape') {
e.preventDefault();
onCancel();
} else if (e.key === 'Tab' && !e.metaKey && !e.ctrlKey) {
// Navigation entre les champs seulement si pas de modificateur
e.preventDefault();
const fields = ['title', 'description', 'tags', 'date'];
const currentIndex = fields.indexOf(activeField || 'title');
const nextField = fields[(currentIndex + 1) % fields.length] as typeof activeField;
setActiveField(nextField);
}
// Laisser passer tous les autres événements (y compris les raccourcis système)
};
const handleTagsChange = (tags: string[]) => {
setFormData(prev => ({
...prev,
tags
}));
};
const handleBlur = (e: React.FocusEvent) => {
// Vérifier si le focus reste dans le composant
setTimeout(() => {
const currentTarget = e.currentTarget;
const relatedTarget = e.relatedTarget as Node | null;
// Si le focus sort complètement du composant ET qu'il n'y a pas de titre
if (currentTarget && (!relatedTarget || !currentTarget.contains(relatedTarget)) && !formData.title.trim()) {
onCancel();
}
}, 100);
};
return (
<div onBlur={handleBlur}>
<Card className="p-3 border-dashed border-[var(--primary)]/30 bg-[var(--card)]/50 hover:border-[var(--primary)]/50 transition-all duration-300">
{/* Header avec titre et priorité */}
<div className="flex items-start gap-2 mb-2 min-w-0">
<input
ref={titleRef}
type="text"
value={formData.title}
onChange={(e) => setFormData(prev => ({ ...prev, title: e.target.value }))}
onKeyDown={(e) => handleKeyDown(e, 'title')}
onFocus={() => setActiveField('title')}
placeholder="Titre de la tâche..."
disabled={isSubmitting}
className="flex-1 min-w-0 bg-transparent border-none outline-none text-[var(--foreground)] font-mono text-sm font-medium placeholder-[var(--muted-foreground)] leading-tight"
/>
{/* Indicateur de priorité */}
<select
value={formData.priority}
onChange={(e) => setFormData(prev => ({ ...prev, priority: e.target.value as TaskPriority }))}
disabled={isSubmitting}
className="flex-shrink-0 w-10 bg-transparent border-none outline-none text-lg text-[var(--muted-foreground)] cursor-pointer text-center"
title={getAllPriorities().find(p => p.key === formData.priority)?.label}
>
{getAllPriorities().map(priorityConfig => (
<option key={priorityConfig.key} value={priorityConfig.key}>
{priorityConfig.icon}
</option>
))}
</select>
</div>
{/* Description */}
<textarea
value={formData.description}
onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))}
onKeyDown={(e) => handleKeyDown(e, 'description')}
onFocus={() => setActiveField('description')}
placeholder="Description..."
rows={2}
disabled={isSubmitting}
className="w-full bg-transparent border-none outline-none text-xs text-[var(--muted-foreground)] font-mono placeholder-[var(--muted-foreground)] resize-none mb-2"
/>
{/* Tags */}
<div className="mb-2">
<TagInput
tags={formData.tags || []}
onChange={handleTagsChange}
placeholder="Tags..."
maxTags={5}
className="text-xs"
compactSuggestions={true}
/>
</div>
{/* Footer avec date et actions */}
<div className="pt-2 border-t border-[var(--border)]/50">
<div className="flex items-center justify-between text-xs min-w-0">
<input
type="datetime-local"
value={formData.dueDate ? new Date(formData.dueDate.getTime() - formData.dueDate.getTimezoneOffset() * 60000).toISOString().slice(0, 16) : ''}
onChange={(e) => setFormData(prev => ({
...prev,
dueDate: e.target.value ? new Date(e.target.value) : undefined
}))}
onFocus={() => setActiveField('date')}
disabled={isSubmitting}
className="bg-transparent border-none outline-none text-[var(--muted-foreground)] font-mono text-xs flex-shrink min-w-0"
/>
{isSubmitting && (
<div className="flex items-center gap-1 text-[var(--primary)] font-mono text-xs flex-shrink-0">
<div className="w-3 h-3 border border-[var(--primary)] border-t-transparent rounded-full animate-spin"></div>
<span>...</span>
</div>
)}
</div>
</div>
</Card>
</div>
);
}