feat: refactor IntegrationFilter for Kanban and Dashboard compatibility

- Updated IntegrationFilter to support both Kanban and Dashboard modes with new filters for manual tasks.
- Replaced SourceQuickFilter with IntegrationFilter in Desktop and Mobile controls for consistency.
- Removed deprecated SourceQuickFilter component to streamline codebase.
- Enhanced task filtering logic to include pinned tasks and manual task visibility.
This commit is contained in:
Julien Froidefond
2025-10-03 08:30:40 +02:00
parent 2137da2ac2
commit 735070dd6f
6 changed files with 164 additions and 237 deletions

View File

@@ -3,12 +3,18 @@
import { useState, useMemo } from 'react'; import { useState, useMemo } from 'react';
import { useTasksContext } from '@/contexts/TasksContext'; import { useTasksContext } from '@/contexts/TasksContext';
import { Dropdown, Button } from '@/components/ui'; import { Dropdown, Button } from '@/components/ui';
import type { KanbanFilters } from '@/lib/types';
interface IntegrationFilterProps { interface IntegrationFilterProps {
selectedSources: string[]; // Interface pour Kanban (nouvelle)
onSourcesChange: (sources: string[]) => void; filters?: KanbanFilters;
hiddenSources: string[]; onFiltersChange?: (filters: KanbanFilters) => void;
onHiddenSourcesChange: (sources: string[]) => void;
// Interface pour Dashboard (ancienne)
selectedSources?: string[];
onSourcesChange?: (sources: string[]) => void;
hiddenSources?: string[];
onHiddenSourcesChange?: (sources: string[]) => void;
} }
interface SourceOption { interface SourceOption {
@@ -20,15 +26,24 @@ interface SourceOption {
type FilterMode = 'all' | 'show' | 'hide'; type FilterMode = 'all' | 'show' | 'hide';
export function IntegrationFilter({ selectedSources, onSourcesChange, hiddenSources, onHiddenSourcesChange }: IntegrationFilterProps) { export function IntegrationFilter({
const { regularTasks } = useTasksContext(); filters,
onFiltersChange,
selectedSources,
onSourcesChange,
hiddenSources,
onHiddenSourcesChange
}: IntegrationFilterProps) {
const { regularTasks, pinnedTasks } = useTasksContext();
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
// Vérifier quelles sources ont des tâches // Vérifier quelles sources ont des tâches (regularTasks + pinnedTasks)
const sources = useMemo((): SourceOption[] => { const sources = useMemo((): SourceOption[] => {
const hasJiraTasks = regularTasks.some(task => task.source === 'jira'); const allTasks = [...regularTasks, ...pinnedTasks];
const hasTfsTasks = regularTasks.some(task => task.source === 'tfs'); const hasJiraTasks = allTasks.some(task => task.source === 'jira');
const hasManualTasks = regularTasks.some(task => task.source === 'manual'); const hasTfsTasks = allTasks.some(task => task.source === 'tfs');
const hasManualTasks = allTasks.some(task => task.source === 'manual');
return [ return [
{ {
@@ -50,7 +65,7 @@ export function IntegrationFilter({ selectedSources, onSourcesChange, hiddenSour
hasTasks: hasManualTasks hasTasks: hasManualTasks
} }
].filter(source => source.hasTasks); ].filter(source => source.hasTasks);
}, [regularTasks]); }, [regularTasks, pinnedTasks]);
// Si aucune source disponible, on n'affiche rien // Si aucune source disponible, on n'affiche rien
if (sources.length === 0) { if (sources.length === 0) {
@@ -58,53 +73,107 @@ export function IntegrationFilter({ selectedSources, onSourcesChange, hiddenSour
} }
const handleModeChange = (sourceId: string, mode: FilterMode) => { // Déterminer le mode d'utilisation (Kanban ou Dashboard)
let newSelectedSources = [...selectedSources]; const isKanbanMode = filters && onFiltersChange;
let newHiddenSources = [...hiddenSources]; const isDashboardMode = selectedSources && onSourcesChange && hiddenSources && onHiddenSourcesChange;
if (mode === 'show') { // Déterminer l'état actuel de chaque source
// Ajouter à selectedSources et retirer de hiddenSources const getSourceMode = (sourceId: 'jira' | 'tfs' | 'manual'): FilterMode => {
if (!newSelectedSources.includes(sourceId)) { if (isKanbanMode && filters) {
newSelectedSources.push(sourceId); if (sourceId === 'jira') {
return filters.showJiraOnly ? 'show' : filters.hideJiraTasks ? 'hide' : 'all';
} else if (sourceId === 'tfs') {
return filters.showTfsOnly ? 'show' : filters.hideTfsTasks ? 'hide' : 'all';
} else { // manual
return filters.showManualOnly ? 'show' : filters.hideManualTasks ? 'hide' : 'all';
} }
newHiddenSources = newHiddenSources.filter(id => id !== sourceId); } else if (isDashboardMode && selectedSources && hiddenSources) {
} else if (mode === 'hide') { if (selectedSources.includes(sourceId)) {
// Ajouter à hiddenSources et retirer de selectedSources return 'show';
if (!newHiddenSources.includes(sourceId)) { } else if (hiddenSources.includes(sourceId)) {
newHiddenSources.push(sourceId); return 'hide';
} else {
return 'all';
} }
newSelectedSources = newSelectedSources.filter(id => id !== sourceId);
} else { // 'all'
// Retirer des deux listes
newSelectedSources = newSelectedSources.filter(id => id !== sourceId);
newHiddenSources = newHiddenSources.filter(id => id !== sourceId);
} }
return 'all';
onHiddenSourcesChange(newHiddenSources); };
onSourcesChange(newSelectedSources);
const handleModeChange = (sourceId: 'jira' | 'tfs' | 'manual', mode: FilterMode) => {
if (isKanbanMode && filters && onFiltersChange) {
const updates: Partial<KanbanFilters> = {};
if (sourceId === 'jira') {
updates.showJiraOnly = mode === 'show';
updates.hideJiraTasks = mode === 'hide';
} else if (sourceId === 'tfs') {
updates.showTfsOnly = mode === 'show';
updates.hideTfsTasks = mode === 'hide';
} else if (sourceId === 'manual') {
updates.showManualOnly = mode === 'show';
updates.hideManualTasks = mode === 'hide';
}
onFiltersChange({ ...filters, ...updates });
} else if (isDashboardMode && onSourcesChange && onHiddenSourcesChange && selectedSources && hiddenSources) {
let newSelectedSources = [...selectedSources];
let newHiddenSources = [...hiddenSources];
if (mode === 'show') {
// Ajouter à selectedSources et retirer de hiddenSources
if (!newSelectedSources.includes(sourceId)) {
newSelectedSources.push(sourceId);
}
newHiddenSources = newHiddenSources.filter(id => id !== sourceId);
} else if (mode === 'hide') {
// Ajouter à hiddenSources et retirer de selectedSources
if (!newHiddenSources.includes(sourceId)) {
newHiddenSources.push(sourceId);
}
newSelectedSources = newSelectedSources.filter(id => id !== sourceId);
} else { // 'all'
// Retirer des deux listes
newSelectedSources = newSelectedSources.filter(id => id !== sourceId);
newHiddenSources = newHiddenSources.filter(id => id !== sourceId);
}
onHiddenSourcesChange(newHiddenSources);
onSourcesChange(newSelectedSources);
}
}; };
// Déterminer la variante du bouton principal
const getMainButtonVariant = () => {
const activeFilters = sources.filter(source => {
const mode = getSourceMode(source.id);
return mode !== 'all';
});
return activeFilters.length === 0 ? 'secondary' : 'selected';
};
const getMainButtonText = () => { const getMainButtonText = () => {
if (selectedSources.length === 0 && hiddenSources.length === 0) { const activeFilters = sources.filter(source => {
return 'Toutes les sources'; const mode = getSourceMode(source.id);
} else if (selectedSources.length === 1 && hiddenSources.length === 0) { return mode !== 'all';
const source = sources.find(s => s.id === selectedSources[0]); });
return source ? `Seulement ${source.label}` : 'Source sélectionnée';
} else if (hiddenSources.length === 1 && selectedSources.length === 0) { if (activeFilters.length === 0) {
const source = sources.find(s => s.id === hiddenSources[0]); return 'Sources';
return source ? `Sans ${source.label}` : 'Source masquée'; } else if (activeFilters.length === 1) {
const source = activeFilters[0];
const mode = getSourceMode(source.id);
return mode === 'show' ? `Seulement ${source.label}` : `Sans ${source.label}`;
} else { } else {
const total = selectedSources.length + hiddenSources.length; return `${activeFilters.length} filtres actifs`;
return `${total} filtres actifs`;
} }
}; };
const dropdownContent = ( const dropdownContent = (
<div className="space-y-3"> <div className="space-y-3">
{sources.map((source) => { {sources.map((source) => {
const isSelected = selectedSources.includes(source.id); const currentMode = getSourceMode(source.id);
const isHidden = hiddenSources.includes(source.id);
return ( return (
<div key={source.id} className="space-y-2"> <div key={source.id} className="space-y-2">
@@ -118,11 +187,13 @@ export function IntegrationFilter({ selectedSources, onSourcesChange, hiddenSour
{/* Bouton Afficher */} {/* Bouton Afficher */}
<button <button
onClick={(e) => { onClick={(e) => {
e.preventDefault();
e.stopPropagation(); e.stopPropagation();
handleModeChange(source.id, 'show'); handleModeChange(source.id, 'show');
}} }}
className={`px-2 py-1 text-xs rounded transition-colors ${ onMouseDown={(e) => e.preventDefault()}
isSelected className={`px-2 py-1 text-xs rounded transition-colors cursor-pointer ${
currentMode === 'show'
? 'bg-[var(--primary)] text-[var(--primary-foreground)]' ? 'bg-[var(--primary)] text-[var(--primary-foreground)]'
: 'bg-[var(--muted)] text-[var(--muted-foreground)] hover:bg-[var(--primary)]/20' : 'bg-[var(--muted)] text-[var(--muted-foreground)] hover:bg-[var(--primary)]/20'
}`} }`}
@@ -134,11 +205,13 @@ export function IntegrationFilter({ selectedSources, onSourcesChange, hiddenSour
{/* Bouton Masquer */} {/* Bouton Masquer */}
<button <button
onClick={(e) => { onClick={(e) => {
e.preventDefault();
e.stopPropagation(); e.stopPropagation();
handleModeChange(source.id, 'hide'); handleModeChange(source.id, 'hide');
}} }}
className={`px-2 py-1 text-xs rounded transition-colors ${ onMouseDown={(e) => e.preventDefault()}
isHidden className={`px-2 py-1 text-xs rounded transition-colors cursor-pointer ${
currentMode === 'hide'
? 'bg-[var(--destructive)] text-white' ? 'bg-[var(--destructive)] text-white'
: 'bg-[var(--muted)] text-[var(--muted-foreground)] hover:bg-[var(--destructive)]/20' : 'bg-[var(--muted)] text-[var(--muted-foreground)] hover:bg-[var(--destructive)]/20'
}`} }`}
@@ -158,13 +231,25 @@ export function IntegrationFilter({ selectedSources, onSourcesChange, hiddenSour
variant="secondary" variant="secondary"
size="sm" size="sm"
onClick={() => { onClick={() => {
onHiddenSourcesChange([]); if (isKanbanMode && filters && onFiltersChange) {
onSourcesChange([]); const updates: Partial<KanbanFilters> = {
showJiraOnly: false,
hideJiraTasks: false,
showTfsOnly: false,
hideTfsTasks: false,
showManualOnly: false,
hideManualTasks: false
};
onFiltersChange({ ...filters, ...updates });
} else if (isDashboardMode && onHiddenSourcesChange && onSourcesChange) {
onHiddenSourcesChange([]);
onSourcesChange([]);
}
}} }}
className="w-full justify-start font-mono" className="w-full justify-start font-mono"
title="Réinitialiser tous les filtres de source" title="Réinitialiser tous les filtres de source"
> >
<span>🔄</span> <span>🔄&nbsp;</span>
<span className="flex-1">Réinitialiser tout</span> <span className="flex-1">Réinitialiser tout</span>
</Button> </Button>
</div> </div>
@@ -176,7 +261,10 @@ export function IntegrationFilter({ selectedSources, onSourcesChange, hiddenSour
open={isOpen} open={isOpen}
onOpenChange={setIsOpen} onOpenChange={setIsOpen}
trigger={`🔗 ${getMainButtonText()}`} trigger={`🔗 ${getMainButtonText()}`}
variant={getMainButtonVariant()}
content={dropdownContent} content={dropdownContent}
placement="bottom-start"
className="min-w-[240px]"
/> />
); );
} }

View File

@@ -2,7 +2,7 @@
import { useState, useEffect, useRef, useCallback } from 'react'; import { useState, useEffect, useRef, useCallback } from 'react';
import { Button, ToggleButton, SearchInput, ControlPanel, ControlSection, ControlGroup } from '@/components/ui'; import { Button, ToggleButton, SearchInput, ControlPanel, ControlSection, ControlGroup } from '@/components/ui';
import { SourceQuickFilter } from '@/components/kanban/SourceQuickFilter'; import { IntegrationFilter } from '@/components/dashboard/IntegrationFilter';
import { FontSizeToggle } from '@/components/ui/FontSizeToggle'; import { FontSizeToggle } from '@/components/ui/FontSizeToggle';
import type { KanbanFilters } from '@/lib/types'; import type { KanbanFilters } from '@/lib/types';
@@ -134,7 +134,7 @@ export function DesktopControls({
<ControlSection className="justify-between lg:justify-start"> <ControlSection className="justify-between lg:justify-start">
<ControlGroup className="border-l border-[var(--border)] ml-2 pl-2 pr-4"> <ControlGroup className="border-l border-[var(--border)] ml-2 pl-2 pr-4">
{/* Raccourcis Sources (Jira & TFS) */} {/* Raccourcis Sources (Jira & TFS) */}
<SourceQuickFilter <IntegrationFilter
filters={kanbanFilters} filters={kanbanFilters}
onFiltersChange={onFiltersChange} onFiltersChange={onFiltersChange}
/> />

View File

@@ -2,7 +2,7 @@
import { useState } from 'react'; import { useState } from 'react';
import { Button, ToggleButton, ControlPanel } from '@/components/ui'; import { Button, ToggleButton, ControlPanel } from '@/components/ui';
import { SourceQuickFilter } from '@/components/kanban/SourceQuickFilter'; import { IntegrationFilter } from '@/components/dashboard/IntegrationFilter';
import { FontSizeToggle } from '@/components/ui/FontSizeToggle'; import { FontSizeToggle } from '@/components/ui/FontSizeToggle';
import type { KanbanFilters } from '@/lib/types'; import type { KanbanFilters } from '@/lib/types';
@@ -149,7 +149,7 @@ export function MobileControls({
Sources Sources
</h3> </h3>
<div className="space-y-2"> <div className="space-y-2">
<SourceQuickFilter <IntegrationFilter
filters={kanbanFilters} filters={kanbanFilters}
onFiltersChange={onFiltersChange} onFiltersChange={onFiltersChange}
/> />

View File

@@ -1,179 +0,0 @@
'use client';
import { useState, useMemo } from 'react';
import { useTasksContext } from '@/contexts/TasksContext';
import { Dropdown, Button } from '@/components/ui';
import type { KanbanFilters } from '@/lib/types';
interface SourceQuickFilterProps {
filters: KanbanFilters;
onFiltersChange: (filters: KanbanFilters) => void;
}
interface SourceOption {
id: 'jira' | 'tfs';
label: string;
icon: string;
hasTasks: boolean;
}
type FilterMode = 'all' | 'show' | 'hide';
export function SourceQuickFilter({ filters, onFiltersChange }: SourceQuickFilterProps) {
const { regularTasks } = useTasksContext();
const [isOpen, setIsOpen] = useState(false);
// Vérifier quelles sources ont des tâches
const sources = useMemo((): SourceOption[] => {
const hasJiraTasks = regularTasks.some(task => task.source === 'jira');
const hasTfsTasks = regularTasks.some(task => task.source === 'tfs');
return [
{
id: 'jira' as const,
label: 'Jira',
icon: '🔹',
hasTasks: hasJiraTasks
},
{
id: 'tfs' as const,
label: 'TFS',
icon: '🔷',
hasTasks: hasTfsTasks
}
].filter(source => source.hasTasks);
}, [regularTasks]);
// Si aucune source disponible, on n'affiche rien
if (sources.length === 0) {
return null;
}
// Déterminer l'état actuel de chaque source
const getSourceMode = (sourceId: 'jira' | 'tfs'): FilterMode => {
if (sourceId === 'jira') {
return filters.showJiraOnly ? 'show' : filters.hideJiraTasks ? 'hide' : 'all';
} else {
return filters.showTfsOnly ? 'show' : filters.hideTfsTasks ? 'hide' : 'all';
}
};
const handleModeChange = (sourceId: 'jira' | 'tfs', mode: FilterMode) => {
const updates: Partial<KanbanFilters> = {};
if (sourceId === 'jira') {
updates.showJiraOnly = mode === 'show';
updates.hideJiraTasks = mode === 'hide';
} else {
updates.showTfsOnly = mode === 'show';
updates.hideTfsTasks = mode === 'hide';
}
onFiltersChange({ ...filters, ...updates });
};
// Déterminer le texte du bouton principal
const getMainButtonVariant = () => {
const activeFilters = sources.filter(source => {
const mode = getSourceMode(source.id);
return mode !== 'all';
});
return activeFilters.length === 0 ? 'secondary' : 'selected';
};
const getMainButtonText = () => {
const activeFilters = sources.filter(source => {
const mode = getSourceMode(source.id);
return mode !== 'all';
});
if (activeFilters.length === 0) {
return 'All sources';
} else if (activeFilters.length === 1) {
const source = activeFilters[0];
const mode = getSourceMode(source.id);
return mode === 'show' ? `${source.label} only` : `No ${source.label}`;
} else {
return `${activeFilters.length} filters`;
}
};
const dropdownContent = (
<div className="space-y-3">
{sources.map((source) => {
const currentMode = getSourceMode(source.id);
return (
<div key={source.id} className="space-y-2">
<div className="flex items-center gap-2 text-sm font-mono text-[var(--muted-foreground)]">
<span>{source.icon}</span>
<span>{source.label}</span>
</div>
<div className="space-y-1 ml-6">
{[
{ mode: 'all' as FilterMode, label: 'Afficher tout', icon: '👁️' },
{ mode: 'show' as FilterMode, label: 'Seulement cette source', icon: '✅' },
{ mode: 'hide' as FilterMode, label: 'Masquer cette source', icon: '🚫' }
].map(({ mode, label, icon }) => (
<label
key={mode}
className="flex items-center gap-2 text-sm cursor-pointer hover:text-[var(--foreground)] transition-colors"
>
<input
type="radio"
name={`source-${source.id}`}
checked={currentMode === mode}
onChange={() => handleModeChange(source.id, mode)}
className="w-3 h-3 text-[var(--primary)] bg-[var(--background)] border-[var(--border)] focus:ring-[var(--primary)]/20"
/>
<span className="flex items-center gap-1">
<span>{icon}</span>
<span>{label}</span>
</span>
</label>
))}
</div>
</div>
);
})}
{/* Option pour réinitialiser tous les filtres */}
<div className="border-t border-[var(--border)] pt-2 mt-2">
<Button
variant="secondary"
size="sm"
onClick={() => {
const updates: Partial<KanbanFilters> = {
showJiraOnly: false,
hideJiraTasks: false,
showTfsOnly: false,
hideTfsTasks: false
};
onFiltersChange({ ...filters, ...updates });
}}
className="w-full justify-start font-mono"
title="Réinitialiser tous les filtres de source"
>
<span>🔄</span>
<span className="flex-1">Réinitialiser tout</span>
</Button>
</div>
</div>
);
return (
<Dropdown
open={isOpen}
onOpenChange={setIsOpen}
trigger={`🔌 ${getMainButtonText()}`}
variant={getMainButtonVariant()}
content={dropdownContent}
placement="bottom-start"
className="min-w-[240px]"
/>
);
}

View File

@@ -70,7 +70,10 @@ export function TasksProvider({ children, initialTasks, initialTags, initialStat
// Filtres TFS // Filtres TFS
showTfsOnly: preferences.kanbanFilters.showTfsOnly || false, showTfsOnly: preferences.kanbanFilters.showTfsOnly || false,
hideTfsTasks: preferences.kanbanFilters.hideTfsTasks || false, hideTfsTasks: preferences.kanbanFilters.hideTfsTasks || false,
tfsProjects: preferences.kanbanFilters.tfsProjects || [] tfsProjects: preferences.kanbanFilters.tfsProjects || [],
// Filtres Manuel
showManualOnly: preferences.kanbanFilters.showManualOnly || false,
hideManualTasks: preferences.kanbanFilters.hideManualTasks || false
}), [preferences]); }), [preferences]);
// Fonction pour mettre à jour les filtres avec persistance // Fonction pour mettre à jour les filtres avec persistance
@@ -92,7 +95,10 @@ export function TasksProvider({ children, initialTasks, initialTags, initialStat
// Filtres TFS // Filtres TFS
showTfsOnly: newFilters.showTfsOnly, showTfsOnly: newFilters.showTfsOnly,
hideTfsTasks: newFilters.hideTfsTasks, hideTfsTasks: newFilters.hideTfsTasks,
tfsProjects: newFilters.tfsProjects tfsProjects: newFilters.tfsProjects,
// Filtres Manuel
showManualOnly: newFilters.showManualOnly,
hideManualTasks: newFilters.hideManualTasks
}; };
const viewPreferenceUpdates = { const viewPreferenceUpdates = {
@@ -151,7 +157,9 @@ export function TasksProvider({ children, initialTasks, initialTags, initialStat
(kanbanFilters.hideJiraTasks ? 1 : 0) + (kanbanFilters.hideJiraTasks ? 1 : 0) +
(kanbanFilters.tfsProjects?.filter(Boolean).length || 0) + (kanbanFilters.tfsProjects?.filter(Boolean).length || 0) +
(kanbanFilters.showTfsOnly ? 1 : 0) + (kanbanFilters.showTfsOnly ? 1 : 0) +
(kanbanFilters.hideTfsTasks ? 1 : 0); (kanbanFilters.hideTfsTasks ? 1 : 0) +
(kanbanFilters.showManualOnly ? 1 : 0) +
(kanbanFilters.hideManualTasks ? 1 : 0);
}, [kanbanFilters]); }, [kanbanFilters]);
// Filtrage et tri des tâches régulières (pas les épinglées) // Filtrage et tri des tâches régulières (pas les épinglées)
@@ -212,6 +220,13 @@ export function TasksProvider({ children, initialTasks, initialTags, initialStat
filtered = filtered.filter(task => task.source !== 'tfs'); filtered = filtered.filter(task => task.source !== 'tfs');
} }
// Filtres spécifiques Manuel
if (kanbanFilters.showManualOnly) {
filtered = filtered.filter(task => task.source === 'manual');
} else if (kanbanFilters.hideManualTasks) {
filtered = filtered.filter(task => task.source !== 'manual');
}
// Filtre par projets TFS // Filtre par projets TFS
if (kanbanFilters.tfsProjects?.length) { if (kanbanFilters.tfsProjects?.length) {
filtered = filtered.filter(task => filtered = filtered.filter(task =>

View File

@@ -92,6 +92,9 @@ export interface KanbanFilters {
showTfsOnly?: boolean; showTfsOnly?: boolean;
hideTfsTasks?: boolean; hideTfsTasks?: boolean;
tfsProjects?: string[]; tfsProjects?: string[];
// Filtres spécifiques Manuel
showManualOnly?: boolean;
hideManualTasks?: boolean;
[key: string]: string | string[] | TaskPriority[] | boolean | undefined; [key: string]: string | string[] | TaskPriority[] | boolean | undefined;
} }