Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions apps/frontend/src/renderer/__tests__/filterUiTasks.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* Tests for the shared board filtering (U3): the pure filter behind the
* BoardToolbar search box and filter chips.
*/

import { describe, expect, it } from 'vitest';
import { filterUiTasks } from '@auto-code/ui';
import type { UiTask } from '@auto-code/ui';

const TASKS: UiTask[] = [
{ id: '001', title: 'Add user authentication', status: 'running', description: 'JWT + refresh flow' },
{ id: '002', title: 'Export usage report', status: 'draft' },
{ id: '003', title: 'Flaky e2e retry', status: 'review', description: 'uploads time out' },
{ id: '004', title: 'Spec index', status: 'done' },
];

describe('filterUiTasks', () => {
it('returns everything for an empty or whitespace query', () => {
expect(filterUiTasks(TASKS, {})).toHaveLength(4);
expect(filterUiTasks(TASKS, { query: ' ' })).toHaveLength(4);
});

it('matches id, title, and description case-insensitively', () => {
expect(filterUiTasks(TASKS, { query: '003' }).map((t) => t.id)).toEqual(['003']);
expect(filterUiTasks(TASKS, { query: 'AUTHENTICATION' }).map((t) => t.id)).toEqual(['001']);
expect(filterUiTasks(TASKS, { query: 'time out' }).map((t) => t.id)).toEqual(['003']);
});

it('narrows by status and combines it with the query', () => {
expect(filterUiTasks(TASKS, { status: 'review' }).map((t) => t.id)).toEqual(['003']);
expect(filterUiTasks(TASKS, { query: 'e', status: 'running' }).map((t) => t.id)).toEqual(['001']);
expect(filterUiTasks(TASKS, { query: 'uploads', status: 'running' })).toHaveLength(0);
});

it('handles tasks without descriptions', () => {
expect(filterUiTasks(TASKS, { query: 'export' }).map((t) => t.id)).toEqual(['002']);
});
});
10 changes: 8 additions & 2 deletions apps/frontend/src/renderer/components/KanbanPilotView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ import { useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
AutoCodeClientProvider,
KanbanBoard as UiKanbanBoard,
BoardView,
TaskDetail as UiTaskDetail,
buildBoardViewLabels,
useTask,
useTasks,
} from '@auto-code/ui';
Expand All @@ -38,6 +39,10 @@ function PilotBoard({ onOpen }: Readonly<{ onOpen: (id: string) => void }>) {
const { t } = useTranslation(['kanban']);
const { tasks, loading, error, reload } = useTasks();
const columns = usePilotColumns();
const labels = useMemo(
() => buildBoardViewLabels(t, 'kanban:pilot.toolbar'),
[t],
);

return (
<div className="h-full overflow-auto p-4">
Expand All @@ -52,9 +57,10 @@ function PilotBoard({ onOpen }: Readonly<{ onOpen: (id: string) => void }>) {
</p>
)}
{!loading && !error && (
<UiKanbanBoard
<BoardView
tasks={tasks}
columns={columns}
labels={labels}
onSelectTask={(task) => onOpen(task.id)}
/>
)}
Expand Down
19 changes: 19 additions & 0 deletions apps/frontend/src/shared/i18n/locales/en/kanban.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,25 @@
"qa_fixing": "QA fixing",
"complete": "Complete",
"failed": "Failed"
},
"toolbar": {
"searchPlaceholder": "Search tasks…",
"searchLabel": "Search tasks by id, title, or description",
"filtersLabel": "Filter tasks",
"viewsLabel": "Select view",
"noMatches": "No tasks match your search or filter",
"matchCount_one": "{{count}} matching task",
"matchCount_other": "{{count}} matching tasks",
"filters": {
"all": "All",
"running": "Running",
"review": "Review"
},
"views": {
"board": "Board",
"table": "Table",
"timeline": "Timeline"
}
}
}
}
19 changes: 19 additions & 0 deletions apps/frontend/src/shared/i18n/locales/fr/kanban.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,25 @@
"qa_fixing": "Correction QA",
"complete": "Terminé",
"failed": "Échoué"
},
"toolbar": {
"searchPlaceholder": "Rechercher des tâches…",
"searchLabel": "Rechercher des tâches par id, titre ou description",
"filtersLabel": "Filtrer les tâches",
"viewsLabel": "Sélectionner la vue",
"noMatches": "Aucune tâche ne correspond à votre recherche ou filtre",
"matchCount_one": "{{count}} tâche correspondante",
"matchCount_other": "{{count}} tâches correspondantes",
"filters": {
"all": "Toutes",
"running": "En cours",
"review": "Revue"
},
"views": {
"board": "Tableau",
"table": "Table",
"timeline": "Chronologie"
}
}
}
}
19 changes: 19 additions & 0 deletions apps/web-frontend/src/i18n/locales/en/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,25 @@
"running": "Running",
"review": "Review",
"done": "Done"
},
"toolbar": {
"searchPlaceholder": "Search tasks…",
"searchLabel": "Search tasks by id, title, or description",
"filtersLabel": "Filter tasks",
"viewsLabel": "Select view",
"noMatches": "No tasks match your search or filter",
"matchCount_one": "{{count}} matching task",
"matchCount_other": "{{count}} matching tasks",
"filters": {
"all": "All",
"running": "Running",
"review": "Review"
},
"views": {
"board": "Board",
"table": "Table",
"timeline": "Timeline"
}
}
}
}
19 changes: 19 additions & 0 deletions apps/web-frontend/src/i18n/locales/fr/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,25 @@
"running": "En cours",
"review": "Revue",
"done": "Terminé"
},
"toolbar": {
"searchPlaceholder": "Rechercher des tâches…",
"searchLabel": "Rechercher des tâches par id, titre ou description",
"filtersLabel": "Filtrer les tâches",
"viewsLabel": "Sélectionner la vue",
"noMatches": "Aucune tâche ne correspond à votre recherche ou filtre",
"matchCount_one": "{{count}} tâche correspondante",
"matchCount_other": "{{count}} tâches correspondantes",
"filters": {
"all": "Toutes",
"running": "En cours",
"review": "Revue"
},
"views": {
"board": "Tableau",
"table": "Table",
"timeline": "Chronologie"
}
}
}
}
18 changes: 12 additions & 6 deletions apps/web-frontend/src/pages/KanbanPilot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,16 @@
* /kanban-next alongside the legacy /kanban until the migration completes.
*/

import { useMemo, useRef } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import type { KanbanColumn, UiTask } from "@auto-code/ui";
import {
AutoCodeClientProvider,
KanbanBoard,
BoardView,
buildBoardViewLabels,
useTasks,
} from "@auto-code/ui";
import type { KanbanColumn, UiTask } from "@auto-code/ui";
import { useMemo, useRef } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { createRestAutoCodeClient } from "../api/autoCodeClient";
import { apiClient } from "../api/client";

Expand All @@ -32,6 +33,10 @@ function PilotBoard() {
],
[t],
);
const labels = useMemo(
() => buildBoardViewLabels(t, "tasks:kanbanPilot.toolbar"),
[t],
);

const handleSelect = (task: UiTask) => {
navigate(`/tasks-next/${task.id}`);
Expand All @@ -50,9 +55,10 @@ function PilotBoard() {
</p>
)}
{!loading && !error && (
<KanbanBoard
<BoardView
tasks={tasks}
columns={columns}
labels={labels}
onSelectTask={handleSelect}
/>
)}
Expand Down
27 changes: 27 additions & 0 deletions libs/ui/src/client/filtering.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { TaskStatus, UiTask } from './types';

export interface UiTaskFilter {
/** Case-insensitive substring match against id, title, and description. */
query?: string;
/** Keep only tasks in this status column. */
status?: TaskStatus;
}

/**
* Client-side board filtering shared by the pilots (search box + filter
* chips). Pure and transport-agnostic: adapters keep delivering the full
* task list; the board narrows what it renders.
*/
export function filterUiTasks(
tasks: readonly UiTask[],
filter: UiTaskFilter,
): UiTask[] {
const query = filter.query?.trim().toLowerCase() ?? '';
return tasks.filter((task) => {
if (filter.status != null && task.status !== filter.status) return false;
if (query === '') return true;
return [task.id, task.title, task.description ?? ''].some((field) =>
field.toLowerCase().includes(query),
);
});
}
47 changes: 47 additions & 0 deletions libs/ui/src/client/useBoardFilter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { useMemo, useState } from 'react';
import { filterUiTasks } from './filtering';
import type { TaskStatus, UiTask } from './types';

/** Board filter chip ids (single-active). */
export type FilterId = 'all' | 'running' | 'review';

/** Ordered filter ids, shared by the toolbar wiring in both pilots. */
export const FILTER_IDS: readonly FilterId[] = ['all', 'running', 'review'];

/** Status behind each filter chip; 'all' clears the narrowing. */
const FILTER_STATUS: Record<FilterId, TaskStatus | undefined> = {
all: undefined,
running: 'running',
review: 'review',
};

export interface UseBoardFilterResult {
query: string;
setQuery: (value: string) => void;
filterId: FilterId;
setFilterId: (id: FilterId) => void;
/** True when a query or a non-'all' chip narrows the board. */
filtering: boolean;
visibleTasks: UiTask[];
}

/**
* Board-narrowing state behind BoardToolbar (search box + single-active
* chips), shared by the pilots so the wiring can't drift between apps.
*/
export function useBoardFilter(tasks: UiTask[]): UseBoardFilterResult {
const [query, setQuery] = useState('');
const [filterId, setFilterId] = useState<FilterId>('all');
const visibleTasks = useMemo(
() => filterUiTasks(tasks, { query, status: FILTER_STATUS[filterId] }),
[tasks, query, filterId],
);
return {
query,
setQuery,
filterId,
setFilterId,
filtering: query.trim() !== '' || filterId !== 'all',
visibleTasks,
};
}
17 changes: 17 additions & 0 deletions libs/ui/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,20 @@ export type { KanbanBoardProps, KanbanColumn } from './screens/KanbanBoard';
// Canonical screens (U2)
export { TaskDetail } from './screens/TaskDetail';
export type { TaskDetailProps } from './screens/TaskDetail';
// Board chrome (U3)
export { BoardToolbar } from './screens/BoardToolbar';
export type {
BoardToolbarProps,
BoardToolbarFilter,
BoardToolbarView,
} from './screens/BoardToolbar';
export { BoardView, buildBoardViewLabels } from './screens/BoardView';
export type {
BoardViewProps,
BoardViewLabels,
Translate,
} from './screens/BoardView';
export { filterUiTasks } from './client/filtering';
export type { UiTaskFilter } from './client/filtering';
export { useBoardFilter, FILTER_IDS } from './client/useBoardFilter';
export type { UseBoardFilterResult, FilterId } from './client/useBoardFilter';
Loading
Loading