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

View File

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

View File

@@ -2,7 +2,7 @@
import { useState } from 'react';
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 type { KanbanFilters } from '@/lib/types';
@@ -149,7 +149,7 @@ export function MobileControls({
Sources
</h3>
<div className="space-y-2">
<SourceQuickFilter
<IntegrationFilter
filters={kanbanFilters}
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
showTfsOnly: preferences.kanbanFilters.showTfsOnly || 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]);
// Fonction pour mettre à jour les filtres avec persistance
@@ -92,7 +95,10 @@ export function TasksProvider({ children, initialTasks, initialTags, initialStat
// Filtres TFS
showTfsOnly: newFilters.showTfsOnly,
hideTfsTasks: newFilters.hideTfsTasks,
tfsProjects: newFilters.tfsProjects
tfsProjects: newFilters.tfsProjects,
// Filtres Manuel
showManualOnly: newFilters.showManualOnly,
hideManualTasks: newFilters.hideManualTasks
};
const viewPreferenceUpdates = {
@@ -151,7 +157,9 @@ export function TasksProvider({ children, initialTasks, initialTags, initialStat
(kanbanFilters.hideJiraTasks ? 1 : 0) +
(kanbanFilters.tfsProjects?.filter(Boolean).length || 0) +
(kanbanFilters.showTfsOnly ? 1 : 0) +
(kanbanFilters.hideTfsTasks ? 1 : 0);
(kanbanFilters.hideTfsTasks ? 1 : 0) +
(kanbanFilters.showManualOnly ? 1 : 0) +
(kanbanFilters.hideManualTasks ? 1 : 0);
}, [kanbanFilters]);
// 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');
}
// 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
if (kanbanFilters.tfsProjects?.length) {
filtered = filtered.filter(task =>

View File

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