- 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.
214 lines
7.9 KiB
TypeScript
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>
|
|
);
|
|
}
|