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:
15
package-lock.json
generated
15
package-lock.json
generated
@@ -15,6 +15,7 @@
|
|||||||
"bcryptjs": "^3.0.2",
|
"bcryptjs": "^3.0.2",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
|
"emoji-regex": "^10.5.0",
|
||||||
"next": "15.5.3",
|
"next": "15.5.3",
|
||||||
"next-auth": "^4.24.11",
|
"next-auth": "^4.24.11",
|
||||||
"prisma": "^6.16.1",
|
"prisma": "^6.16.1",
|
||||||
@@ -3807,10 +3808,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/emoji-regex": {
|
"node_modules/emoji-regex": {
|
||||||
"version": "9.2.2",
|
"version": "10.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.5.0.tgz",
|
||||||
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
|
"integrity": "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/empathic": {
|
"node_modules/empathic": {
|
||||||
@@ -4336,6 +4336,13 @@
|
|||||||
"eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9"
|
"eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/eslint-plugin-jsx-a11y/node_modules/emoji-regex": {
|
||||||
|
"version": "9.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
||||||
|
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/eslint-plugin-react": {
|
"node_modules/eslint-plugin-react": {
|
||||||
"version": "7.37.5",
|
"version": "7.37.5",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz",
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
"bcryptjs": "^3.0.2",
|
"bcryptjs": "^3.0.2",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
|
"emoji-regex": "^10.5.0",
|
||||||
"next": "15.5.3",
|
"next": "15.5.3",
|
||||||
"next-auth": "^4.24.11",
|
"next-auth": "^4.24.11",
|
||||||
"prisma": "^6.16.1",
|
"prisma": "^6.16.1",
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ model Task {
|
|||||||
tfsRepository String?
|
tfsRepository String?
|
||||||
tfsSourceBranch String?
|
tfsSourceBranch String?
|
||||||
tfsTargetBranch String?
|
tfsTargetBranch String?
|
||||||
|
primaryTagId String?
|
||||||
|
primaryTag Tag? @relation("PrimaryTag", fields: [primaryTagId], references: [id])
|
||||||
dailyCheckboxes DailyCheckbox[]
|
dailyCheckboxes DailyCheckbox[]
|
||||||
taskTags TaskTag[]
|
taskTags TaskTag[]
|
||||||
|
|
||||||
@@ -54,11 +56,12 @@ model Task {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model Tag {
|
model Tag {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String @unique
|
name String @unique
|
||||||
color String @default("#6b7280")
|
color String @default("#6b7280")
|
||||||
isPinned Boolean @default(false)
|
isPinned Boolean @default(false)
|
||||||
taskTags TaskTag[]
|
taskTags TaskTag[]
|
||||||
|
primaryTasks Task[] @relation("PrimaryTag")
|
||||||
|
|
||||||
@@map("tags")
|
@@map("tags")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ export async function updateTask(data: {
|
|||||||
status?: TaskStatus;
|
status?: TaskStatus;
|
||||||
priority?: TaskPriority;
|
priority?: TaskPriority;
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
|
primaryTagId?: string;
|
||||||
dueDate?: Date;
|
dueDate?: Date;
|
||||||
}): Promise<ActionResult> {
|
}): Promise<ActionResult> {
|
||||||
try {
|
try {
|
||||||
@@ -109,6 +110,7 @@ export async function updateTask(data: {
|
|||||||
if (data.status !== undefined) updateData.status = data.status;
|
if (data.status !== undefined) updateData.status = data.status;
|
||||||
if (data.priority !== undefined) updateData.priority = data.priority;
|
if (data.priority !== undefined) updateData.priority = data.priority;
|
||||||
if (data.tags !== undefined) updateData.tags = data.tags;
|
if (data.tags !== undefined) updateData.tags = data.tags;
|
||||||
|
if (data.primaryTagId !== undefined) updateData.primaryTagId = data.primaryTagId;
|
||||||
if (data.dueDate !== undefined) updateData.dueDate = data.dueDate;
|
if (data.dueDate !== undefined) updateData.dueDate = data.dueDate;
|
||||||
|
|
||||||
const task = await tasksService.updateTask(data.taskId, updateData);
|
const task = await tasksService.updateTask(data.taskId, updateData);
|
||||||
@@ -136,6 +138,7 @@ export async function createTask(data: {
|
|||||||
status?: TaskStatus;
|
status?: TaskStatus;
|
||||||
priority?: TaskPriority;
|
priority?: TaskPriority;
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
|
primaryTagId?: string;
|
||||||
}): Promise<ActionResult> {
|
}): Promise<ActionResult> {
|
||||||
try {
|
try {
|
||||||
if (!data.title.trim()) {
|
if (!data.title.trim()) {
|
||||||
@@ -147,7 +150,8 @@ export async function createTask(data: {
|
|||||||
description: data.description?.trim() || '',
|
description: data.description?.trim() || '',
|
||||||
status: data.status || 'todo',
|
status: data.status || 'todo',
|
||||||
priority: data.priority || 'medium',
|
priority: data.priority || 'medium',
|
||||||
tags: data.tags || []
|
tags: data.tags || [],
|
||||||
|
primaryTagId: data.primaryTagId
|
||||||
});
|
});
|
||||||
|
|
||||||
// Revalidation automatique du cache
|
// Revalidation automatique du cache
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ interface EditTaskFormProps {
|
|||||||
status?: TaskStatus;
|
status?: TaskStatus;
|
||||||
priority?: TaskPriority;
|
priority?: TaskPriority;
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
|
primaryTagId?: string;
|
||||||
dueDate?: Date;
|
dueDate?: Date;
|
||||||
}) => Promise<void>;
|
}) => Promise<void>;
|
||||||
task: Task | null;
|
task: Task | null;
|
||||||
@@ -38,6 +39,7 @@ export function EditTaskForm({
|
|||||||
status: TaskStatus;
|
status: TaskStatus;
|
||||||
priority: TaskPriority;
|
priority: TaskPriority;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
|
primaryTagId?: string;
|
||||||
dueDate?: Date;
|
dueDate?: Date;
|
||||||
}>({
|
}>({
|
||||||
title: '',
|
title: '',
|
||||||
@@ -45,6 +47,7 @@ export function EditTaskForm({
|
|||||||
status: 'todo' as TaskStatus,
|
status: 'todo' as TaskStatus,
|
||||||
priority: 'medium' as TaskPriority,
|
priority: 'medium' as TaskPriority,
|
||||||
tags: [],
|
tags: [],
|
||||||
|
primaryTagId: undefined,
|
||||||
dueDate: undefined,
|
dueDate: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -59,6 +62,7 @@ export function EditTaskForm({
|
|||||||
status: task.status,
|
status: task.status,
|
||||||
priority: task.priority,
|
priority: task.priority,
|
||||||
tags: task.tags || [],
|
tags: task.tags || [],
|
||||||
|
primaryTagId: task.primaryTagId,
|
||||||
dueDate: task.dueDate,
|
dueDate: task.dueDate,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -148,7 +152,9 @@ export function EditTaskForm({
|
|||||||
<TaskTagsSection
|
<TaskTagsSection
|
||||||
taskId={task.id}
|
taskId={task.id}
|
||||||
tags={formData.tags}
|
tags={formData.tags}
|
||||||
|
primaryTagId={formData.primaryTagId}
|
||||||
onTagsChange={(tags) => setFormData((prev) => ({ ...prev, tags }))}
|
onTagsChange={(tags) => setFormData((prev) => ({ ...prev, tags }))}
|
||||||
|
onPrimaryTagChange={(primaryTagId) => setFormData((prev) => ({ ...prev, primaryTagId }))}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
|
|||||||
@@ -6,20 +6,33 @@ import { RelatedTodos } from '@/components/forms/RelatedTodos';
|
|||||||
interface TaskTagsSectionProps {
|
interface TaskTagsSectionProps {
|
||||||
taskId: string;
|
taskId: string;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
|
primaryTagId?: string;
|
||||||
onTagsChange: (tags: string[]) => void;
|
onTagsChange: (tags: string[]) => void;
|
||||||
|
onPrimaryTagChange: (tagId: string | undefined) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TaskTagsSection({ taskId, tags, onTagsChange }: TaskTagsSectionProps) {
|
export function TaskTagsSection({
|
||||||
|
taskId,
|
||||||
|
tags,
|
||||||
|
primaryTagId,
|
||||||
|
onTagsChange,
|
||||||
|
onPrimaryTagChange
|
||||||
|
}: TaskTagsSectionProps) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Tags */}
|
{/* Tags */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<label className="block text-sm font-mono font-medium text-[var(--muted-foreground)] uppercase tracking-wider">
|
<label className="block text-sm font-mono font-medium text-[var(--muted-foreground)] uppercase tracking-wider">
|
||||||
Tags
|
Tags
|
||||||
|
<span className="text-xs normal-case ml-2 text-[var(--muted-foreground)]">
|
||||||
|
(cliquer sur un tag pour le sélectionner comme prioritaire)
|
||||||
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<TagInput
|
<TagInput
|
||||||
tags={tags || []}
|
tags={tags || []}
|
||||||
|
primaryTagId={primaryTagId}
|
||||||
|
onPrimaryTagChange={onPrimaryTagChange}
|
||||||
onChange={onTagsChange}
|
onChange={onTagsChange}
|
||||||
placeholder="Ajouter des tags..."
|
placeholder="Ajouter des tags..."
|
||||||
maxTags={10}
|
maxTags={10}
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ export function TaskCard({ task, onEdit, compactView = false }: TaskCardProps) {
|
|||||||
title={task.title}
|
title={task.title}
|
||||||
description={task.description}
|
description={task.description}
|
||||||
tags={task.tags}
|
tags={task.tags}
|
||||||
|
primaryTagId={task.primaryTagId}
|
||||||
priority={task.priority}
|
priority={task.priority}
|
||||||
status={task.status}
|
status={task.status}
|
||||||
dueDate={task.dueDate}
|
dueDate={task.dueDate}
|
||||||
|
|||||||
@@ -156,4 +156,4 @@ export function TagList({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -4,11 +4,14 @@ import { useState, useRef, useEffect } from 'react';
|
|||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import { Tag } from '@/lib/types';
|
import { Tag } from '@/lib/types';
|
||||||
import { useTagsAutocomplete } from '@/hooks/useTags';
|
import { useTagsAutocomplete } from '@/hooks/useTags';
|
||||||
|
import { tagsClient } from '@/clients/tags-client';
|
||||||
import { Badge } from './Badge';
|
import { Badge } from './Badge';
|
||||||
|
|
||||||
interface TagInputProps {
|
interface TagInputProps {
|
||||||
tags: string[];
|
tags: string[];
|
||||||
onChange: (tags: string[]) => void;
|
onChange: (tags: string[]) => void;
|
||||||
|
primaryTagId?: string;
|
||||||
|
onPrimaryTagChange?: (tagId: string | undefined) => void;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
maxTags?: number;
|
maxTags?: number;
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -18,6 +21,8 @@ interface TagInputProps {
|
|||||||
export function TagInput({
|
export function TagInput({
|
||||||
tags,
|
tags,
|
||||||
onChange,
|
onChange,
|
||||||
|
primaryTagId,
|
||||||
|
onPrimaryTagChange,
|
||||||
placeholder = "Ajouter des tags...",
|
placeholder = "Ajouter des tags...",
|
||||||
maxTags = 10,
|
maxTags = 10,
|
||||||
className = "",
|
className = "",
|
||||||
@@ -32,6 +37,21 @@ export function TagInput({
|
|||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const { suggestions, loading, searchTags, clearSuggestions, loadPopularTags } = useTagsAutocomplete();
|
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
|
// Calculer la position du dropdown
|
||||||
const updateDropdownPosition = () => {
|
const updateDropdownPosition = () => {
|
||||||
@@ -87,6 +107,28 @@ export function TagInput({
|
|||||||
|
|
||||||
const removeTag = (tagToRemove: string) => {
|
const removeTag = (tagToRemove: string) => {
|
||||||
onChange(tags.filter(tag => tag !== tagToRemove));
|
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) => {
|
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="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">
|
<div className="flex flex-wrap gap-1 items-center">
|
||||||
{/* Tags existants */}
|
{/* Tags existants */}
|
||||||
{tags.map((tag, index) => (
|
{tags.map((tag, index) => {
|
||||||
<Badge
|
const tagObj = allTags?.find(t => t.name === tag);
|
||||||
key={index}
|
const isPrimary = tagObj && primaryTagId === tagObj.id;
|
||||||
variant="default"
|
|
||||||
className="flex items-center gap-1 px-2 py-1 text-xs"
|
return (
|
||||||
>
|
<Badge
|
||||||
<span>{tag}</span>
|
key={index}
|
||||||
<button
|
variant="default"
|
||||||
type="button"
|
className={`flex items-center gap-1 px-2 py-1 text-xs transition-all ${
|
||||||
onClick={() => removeTag(tag)}
|
isPrimary
|
||||||
className="text-[var(--muted-foreground)] hover:text-[var(--foreground)] ml-1"
|
? 'ring-2 ring-[var(--primary)] bg-[var(--primary)]/10'
|
||||||
aria-label={`Supprimer le tag ${tag}`}
|
: 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}
|
||||||
>
|
>
|
||||||
×
|
<span>{tag}</span>
|
||||||
</button>
|
{isPrimary && <span className="text-[var(--primary)] font-bold">★</span>}
|
||||||
</Badge>
|
<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 */}
|
{/* Input pour nouveau tag */}
|
||||||
{tags.length < maxTags && (
|
{tags.length < maxTags && (
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Card } from './Card';
|
|||||||
import { Badge } from './Badge';
|
import { Badge } from './Badge';
|
||||||
import { TagDisplay } from './TagDisplay';
|
import { TagDisplay } from './TagDisplay';
|
||||||
import { formatDateForDisplay } from '@/lib/date-utils';
|
import { formatDateForDisplay } from '@/lib/date-utils';
|
||||||
|
import emojiRegex from 'emoji-regex';
|
||||||
|
|
||||||
interface TaskCardProps extends HTMLAttributes<HTMLDivElement> {
|
interface TaskCardProps extends HTMLAttributes<HTMLDivElement> {
|
||||||
// Variants
|
// Variants
|
||||||
@@ -14,6 +15,7 @@ interface TaskCardProps extends HTMLAttributes<HTMLDivElement> {
|
|||||||
title: string;
|
title: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
|
primaryTagId?: string; // ID du tag prioritaire
|
||||||
priority?: 'low' | 'medium' | 'high' | 'urgent';
|
priority?: 'low' | 'medium' | 'high' | 'urgent';
|
||||||
|
|
||||||
// Status & metadata
|
// Status & metadata
|
||||||
@@ -56,6 +58,7 @@ const TaskCard = forwardRef<HTMLDivElement, TaskCardProps>(
|
|||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
tags = [],
|
tags = [],
|
||||||
|
primaryTagId,
|
||||||
priority = 'medium',
|
priority = 'medium',
|
||||||
status,
|
status,
|
||||||
dueDate,
|
dueDate,
|
||||||
@@ -203,18 +206,30 @@ const TaskCard = forwardRef<HTMLDivElement, TaskCardProps>(
|
|||||||
return colors[priority as keyof typeof colors] || colors.medium;
|
return colors[priority as keyof typeof colors] || colors.medium;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Extraire les emojis du titre
|
// Fonction pour extraire les emojis avec la lib emoji-regex
|
||||||
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 extractEmojis = (text: string): string[] => {
|
||||||
const titleEmojis = title.match(emojiRegex) || [];
|
const regex = emojiRegex();
|
||||||
const titleWithoutEmojis = title.replace(emojiRegex, '').trim();
|
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;
|
let displayEmojis: string[] = titleEmojis;
|
||||||
if (displayEmojis.length === 0 && tags && tags.length > 0) {
|
if (displayEmojis.length === 0 && tags && tags.length > 0) {
|
||||||
const firstTag = availableTags.find((tag) => tag.name === tags[0]);
|
// Priorité au tag prioritaire, sinon premier tag
|
||||||
if (firstTag) {
|
let tagToUse = null;
|
||||||
const tagEmojis = firstTag.name.match(emojiRegex);
|
if (primaryTagId && availableTags) {
|
||||||
if (tagEmojis && tagEmojis.length > 0) {
|
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
|
displayEmojis = [tagEmojis[0]]; // Prendre seulement le premier emoji du tag
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ export interface Task {
|
|||||||
sourceId?: string;
|
sourceId?: string;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
tagDetails?: Tag[]; // Tags avec informations complètes (isPinned, etc.)
|
tagDetails?: Tag[]; // Tags avec informations complètes (isPinned, etc.)
|
||||||
|
primaryTagId?: string; // ID du tag prioritaire à afficher en premier
|
||||||
|
primaryTag?: Tag; // Tag prioritaire avec informations complètes
|
||||||
dueDate?: Date;
|
dueDate?: Date;
|
||||||
completedAt?: Date;
|
completedAt?: Date;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export class TasksService {
|
|||||||
tag: true
|
tag: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
primaryTag: true,
|
||||||
_count: {
|
_count: {
|
||||||
select: {
|
select: {
|
||||||
dailyCheckboxes: true
|
dailyCheckboxes: true
|
||||||
@@ -65,6 +66,7 @@ export class TasksService {
|
|||||||
status?: TaskStatus;
|
status?: TaskStatus;
|
||||||
priority?: TaskPriority;
|
priority?: TaskPriority;
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
|
primaryTagId?: string;
|
||||||
dueDate?: Date;
|
dueDate?: Date;
|
||||||
}): Promise<Task> {
|
}): Promise<Task> {
|
||||||
const status = taskData.status || 'todo';
|
const status = taskData.status || 'todo';
|
||||||
@@ -75,6 +77,7 @@ export class TasksService {
|
|||||||
status: status,
|
status: status,
|
||||||
priority: taskData.priority || 'medium',
|
priority: taskData.priority || 'medium',
|
||||||
dueDate: taskData.dueDate,
|
dueDate: taskData.dueDate,
|
||||||
|
primaryTagId: taskData.primaryTagId,
|
||||||
source: 'manual', // Source manuelle
|
source: 'manual', // Source manuelle
|
||||||
sourceId: `manual-${Date.now()}`, // ID unique
|
sourceId: `manual-${Date.now()}`, // ID unique
|
||||||
// Si créée directement en done/archived, définir completedAt
|
// Si créée directement en done/archived, définir completedAt
|
||||||
@@ -85,7 +88,8 @@ export class TasksService {
|
|||||||
include: {
|
include: {
|
||||||
tag: true
|
tag: true
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
primaryTag: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -102,7 +106,8 @@ export class TasksService {
|
|||||||
include: {
|
include: {
|
||||||
tag: true
|
tag: true
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
primaryTag: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -118,6 +123,7 @@ export class TasksService {
|
|||||||
status?: TaskStatus;
|
status?: TaskStatus;
|
||||||
priority?: TaskPriority;
|
priority?: TaskPriority;
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
|
primaryTagId?: string;
|
||||||
dueDate?: Date;
|
dueDate?: Date;
|
||||||
}): Promise<Task> {
|
}): Promise<Task> {
|
||||||
const task = await prisma.task.findUnique({
|
const task = await prisma.task.findUnique({
|
||||||
@@ -129,12 +135,13 @@ export class TasksService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Logique métier : si on marque comme terminé, on ajoute la date
|
// Logique métier : si on marque comme terminé, on ajoute la date
|
||||||
const updateData: Prisma.TaskUpdateInput = {
|
const updateData: Prisma.TaskUpdateInput & { primaryTagId?: string } = {
|
||||||
title: updates.title,
|
title: updates.title,
|
||||||
description: updates.description,
|
description: updates.description,
|
||||||
status: updates.status,
|
status: updates.status,
|
||||||
priority: updates.priority,
|
priority: updates.priority,
|
||||||
dueDate: updates.dueDate,
|
dueDate: updates.dueDate,
|
||||||
|
primaryTagId: updates.primaryTagId,
|
||||||
updatedAt: getToday()
|
updatedAt: getToday()
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -151,7 +158,7 @@ export class TasksService {
|
|||||||
|
|
||||||
await prisma.task.update({
|
await prisma.task.update({
|
||||||
where: { id: taskId },
|
where: { id: taskId },
|
||||||
data: updateData
|
data: updateData as Prisma.TaskUpdateInput
|
||||||
});
|
});
|
||||||
|
|
||||||
// Mettre à jour les relations avec les tags
|
// Mettre à jour les relations avec les tags
|
||||||
@@ -167,7 +174,8 @@ export class TasksService {
|
|||||||
include: {
|
include: {
|
||||||
tag: true
|
tag: true
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
primaryTag: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -389,6 +397,13 @@ export class TasksService {
|
|||||||
sourceId: prismaTask.sourceId ?? undefined,
|
sourceId: prismaTask.sourceId ?? undefined,
|
||||||
tags: tags,
|
tags: tags,
|
||||||
tagDetails: tagDetails,
|
tagDetails: tagDetails,
|
||||||
|
primaryTagId: prismaTask.primaryTagId ?? undefined,
|
||||||
|
primaryTag: ('primaryTag' in prismaTask && prismaTask.primaryTag) ? {
|
||||||
|
id: (prismaTask.primaryTag as { id: string }).id,
|
||||||
|
name: (prismaTask.primaryTag as { name: string }).name,
|
||||||
|
color: (prismaTask.primaryTag as { color: string }).color,
|
||||||
|
isPinned: (prismaTask.primaryTag as { isPinned: boolean }).isPinned
|
||||||
|
} : undefined,
|
||||||
dueDate: prismaTask.dueDate ?? undefined,
|
dueDate: prismaTask.dueDate ?? undefined,
|
||||||
completedAt: prismaTask.completedAt ?? undefined,
|
completedAt: prismaTask.completedAt ?? undefined,
|
||||||
createdAt: prismaTask.createdAt,
|
createdAt: prismaTask.createdAt,
|
||||||
|
|||||||
Reference in New Issue
Block a user