feat: add primary tag functionality to tasks
- Introduced `primaryTagId` to `Task` model and updated related components to support selecting a primary tag. - Enhanced `TaskCard`, `EditTaskForm`, and `TagInput` to handle primary tag selection and display. - Updated `TasksService` to manage primary tag data during task creation and updates. - Added `emoji-regex` dependency for improved emoji handling in task titles.
This commit is contained in:
@@ -156,4 +156,4 @@ export function TagList({
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,14 @@ import { useState, useRef, useEffect } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { Tag } from '@/lib/types';
|
||||
import { useTagsAutocomplete } from '@/hooks/useTags';
|
||||
import { tagsClient } from '@/clients/tags-client';
|
||||
import { Badge } from './Badge';
|
||||
|
||||
interface TagInputProps {
|
||||
tags: string[];
|
||||
onChange: (tags: string[]) => void;
|
||||
primaryTagId?: string;
|
||||
onPrimaryTagChange?: (tagId: string | undefined) => void;
|
||||
placeholder?: string;
|
||||
maxTags?: number;
|
||||
className?: string;
|
||||
@@ -18,6 +21,8 @@ interface TagInputProps {
|
||||
export function TagInput({
|
||||
tags,
|
||||
onChange,
|
||||
primaryTagId,
|
||||
onPrimaryTagChange,
|
||||
placeholder = "Ajouter des tags...",
|
||||
maxTags = 10,
|
||||
className = "",
|
||||
@@ -32,6 +37,21 @@ export function TagInput({
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { suggestions, loading, searchTags, clearSuggestions, loadPopularTags } = useTagsAutocomplete();
|
||||
const [allTags, setAllTags] = useState<Tag[]>([]);
|
||||
|
||||
// Charger tous les tags au début pour pouvoir identifier le tag prioritaire
|
||||
useEffect(() => {
|
||||
const loadTags = async () => {
|
||||
try {
|
||||
const response = await tagsClient.getPopularTags(100);
|
||||
setAllTags(response.data);
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du chargement des tags:', error);
|
||||
setAllTags([]);
|
||||
}
|
||||
};
|
||||
loadTags();
|
||||
}, []);
|
||||
|
||||
// Calculer la position du dropdown
|
||||
const updateDropdownPosition = () => {
|
||||
@@ -87,6 +107,28 @@ export function TagInput({
|
||||
|
||||
const removeTag = (tagToRemove: string) => {
|
||||
onChange(tags.filter(tag => tag !== tagToRemove));
|
||||
// Si on supprime le tag prioritaire, le désélectionner
|
||||
if (primaryTagId && onPrimaryTagChange && allTags) {
|
||||
const tagToRemoveObj = allTags.find(tag => tag.name === tagToRemove);
|
||||
if (tagToRemoveObj && tagToRemoveObj.id === primaryTagId) {
|
||||
onPrimaryTagChange(undefined);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleTagClick = (tagName: string) => {
|
||||
if (!onPrimaryTagChange || !allTags) return;
|
||||
|
||||
const tagObj = allTags.find(tag => tag.name === tagName);
|
||||
if (!tagObj) return;
|
||||
|
||||
if (primaryTagId === tagObj.id) {
|
||||
// Désélectionner si c'est déjà sélectionné
|
||||
onPrimaryTagChange(undefined);
|
||||
} else {
|
||||
// Sélectionner comme prioritaire
|
||||
onPrimaryTagChange(tagObj.id);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
@@ -147,23 +189,40 @@ export function TagInput({
|
||||
<div className="min-h-[42px] p-2 border border-[var(--border)] rounded-lg bg-[var(--input)] focus-within:border-[var(--primary)] focus-within:ring-1 focus-within:ring-[var(--primary)]/20 transition-colors">
|
||||
<div className="flex flex-wrap gap-1 items-center">
|
||||
{/* Tags existants */}
|
||||
{tags.map((tag, index) => (
|
||||
<Badge
|
||||
key={index}
|
||||
variant="default"
|
||||
className="flex items-center gap-1 px-2 py-1 text-xs"
|
||||
>
|
||||
<span>{tag}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeTag(tag)}
|
||||
className="text-[var(--muted-foreground)] hover:text-[var(--foreground)] ml-1"
|
||||
aria-label={`Supprimer le tag ${tag}`}
|
||||
{tags.map((tag, index) => {
|
||||
const tagObj = allTags?.find(t => t.name === tag);
|
||||
const isPrimary = tagObj && primaryTagId === tagObj.id;
|
||||
|
||||
return (
|
||||
<Badge
|
||||
key={index}
|
||||
variant="default"
|
||||
className={`flex items-center gap-1 px-2 py-1 text-xs transition-all ${
|
||||
isPrimary
|
||||
? 'ring-2 ring-[var(--primary)] bg-[var(--primary)]/10'
|
||||
: onPrimaryTagChange
|
||||
? 'cursor-pointer hover:ring-1 hover:ring-[var(--primary)]/50'
|
||||
: ''
|
||||
}`}
|
||||
onClick={onPrimaryTagChange ? () => handleTagClick(tag) : undefined}
|
||||
title={onPrimaryTagChange ? (isPrimary ? 'Tag prioritaire (cliquer pour désélectionner)' : 'Cliquer pour sélectionner comme prioritaire') : undefined}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
<span>{tag}</span>
|
||||
{isPrimary && <span className="text-[var(--primary)] font-bold">★</span>}
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
removeTag(tag);
|
||||
}}
|
||||
className="text-[var(--muted-foreground)] hover:text-[var(--foreground)] ml-1"
|
||||
aria-label={`Supprimer le tag ${tag}`}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</Badge>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Input pour nouveau tag */}
|
||||
{tags.length < maxTags && (
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Card } from './Card';
|
||||
import { Badge } from './Badge';
|
||||
import { TagDisplay } from './TagDisplay';
|
||||
import { formatDateForDisplay } from '@/lib/date-utils';
|
||||
import emojiRegex from 'emoji-regex';
|
||||
|
||||
interface TaskCardProps extends HTMLAttributes<HTMLDivElement> {
|
||||
// Variants
|
||||
@@ -14,6 +15,7 @@ interface TaskCardProps extends HTMLAttributes<HTMLDivElement> {
|
||||
title: string;
|
||||
description?: string;
|
||||
tags?: string[];
|
||||
primaryTagId?: string; // ID du tag prioritaire
|
||||
priority?: 'low' | 'medium' | 'high' | 'urgent';
|
||||
|
||||
// Status & metadata
|
||||
@@ -56,6 +58,7 @@ const TaskCard = forwardRef<HTMLDivElement, TaskCardProps>(
|
||||
title,
|
||||
description,
|
||||
tags = [],
|
||||
primaryTagId,
|
||||
priority = 'medium',
|
||||
status,
|
||||
dueDate,
|
||||
@@ -203,18 +206,30 @@ const TaskCard = forwardRef<HTMLDivElement, TaskCardProps>(
|
||||
return colors[priority as keyof typeof colors] || colors.medium;
|
||||
};
|
||||
|
||||
// Extraire les emojis du titre
|
||||
const emojiRegex = /(?:[\u{1F600}-\u{1F64F}]|[\u{1F300}-\u{1F5FF}]|[\u{1F680}-\u{1F6FF}]|[\u{1F900}-\u{1F9FF}]|[\u{1F1E0}-\u{1F1FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}])(?:[\u{200D}][\u{1F600}-\u{1F64F}]|[\u{1F300}-\u{1F5FF}]|[\u{1F680}-\u{1F6FF}]|[\u{1F900}-\u{1F9FF}]|[\u{1F1E0}-\u{1F1FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]|[\u{FE0F}])*/gu;
|
||||
const titleEmojis = title.match(emojiRegex) || [];
|
||||
const titleWithoutEmojis = title.replace(emojiRegex, '').trim();
|
||||
// Fonction pour extraire les emojis avec la lib emoji-regex
|
||||
const extractEmojis = (text: string): string[] => {
|
||||
const regex = emojiRegex();
|
||||
return text.match(regex) || [];
|
||||
};
|
||||
|
||||
// Si pas d'emoji dans le titre, utiliser l'emoji du premier tag
|
||||
const titleEmojis = extractEmojis(title);
|
||||
const titleWithoutEmojis = title.replace(emojiRegex(), '').trim();
|
||||
|
||||
// Si pas d'emoji dans le titre, utiliser l'emoji du tag prioritaire ou du premier tag
|
||||
let displayEmojis: string[] = titleEmojis;
|
||||
if (displayEmojis.length === 0 && tags && tags.length > 0) {
|
||||
const firstTag = availableTags.find((tag) => tag.name === tags[0]);
|
||||
if (firstTag) {
|
||||
const tagEmojis = firstTag.name.match(emojiRegex);
|
||||
if (tagEmojis && tagEmojis.length > 0) {
|
||||
// Priorité au tag prioritaire, sinon premier tag
|
||||
let tagToUse = null;
|
||||
if (primaryTagId && availableTags) {
|
||||
tagToUse = availableTags.find((tag) => tag.id === primaryTagId);
|
||||
}
|
||||
if (!tagToUse) {
|
||||
tagToUse = availableTags.find((tag) => tag.name === tags[0]);
|
||||
}
|
||||
|
||||
if (tagToUse) {
|
||||
const tagEmojis = extractEmojis(tagToUse.name);
|
||||
if (tagEmojis.length > 0) {
|
||||
displayEmojis = [tagEmojis[0]]; // Prendre seulement le premier emoji du tag
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user