From 2c084803329b342411facd79cbe7b647c5d02b6b Mon Sep 17 00:00:00 2001 From: rafavalls Date: Fri, 24 Apr 2026 03:33:29 -0300 Subject: [PATCH 1/2] feat(tasks-panel): move task and automation filters server-side Task member/type filters now drive useTasks query params instead of filtering already-fetched data in the component. Automation name search uses an ILIKE query via the AUTOMATION_LIST tool instead of client-side array filter. Co-Authored-By: Claude Sonnet 4.6 --- apps/mesh/src/storage/automations.ts | 6 ++++ apps/mesh/src/tools/automations/list.ts | 2 ++ apps/mesh/src/web/hooks/use-automations.ts | 8 +++-- .../src/web/layouts/tasks-panel/index.tsx | 36 ++++++++++++++----- .../web/layouts/tasks-panel/tasks-section.tsx | 30 ++++++---------- apps/mesh/src/web/lib/query-keys.ts | 13 +++++-- .../views/automations/automations-list.tsx | 27 ++++++++------ 7 files changed, 80 insertions(+), 42 deletions(-) diff --git a/apps/mesh/src/storage/automations.ts b/apps/mesh/src/storage/automations.ts index f1e4ab261f..c9779ca7f7 100644 --- a/apps/mesh/src/storage/automations.ts +++ b/apps/mesh/src/storage/automations.ts @@ -66,6 +66,7 @@ export interface AutomationsStorage { listWithTriggerCounts( organizationId: string, virtualMcpId?: string | null, + search?: string | null, ): Promise; update( id: string, @@ -244,6 +245,7 @@ class KyselyAutomationsStorage implements AutomationsStorage { async listWithTriggerCounts( organizationId: string, virtualMcpId?: string | null, + search?: string | null, ): Promise { let query = this.db .selectFrom("automations as a") @@ -272,6 +274,10 @@ class KyselyAutomationsStorage implements AutomationsStorage { : query.where("a.virtual_mcp_id", "is", null); } + if (search) { + query = query.where("a.name", "ilike", `%${search}%`); + } + const rows = await query .groupBy([ "a.id", diff --git a/apps/mesh/src/tools/automations/list.ts b/apps/mesh/src/tools/automations/list.ts index ad772b3bb2..7041481e20 100644 --- a/apps/mesh/src/tools/automations/list.ts +++ b/apps/mesh/src/tools/automations/list.ts @@ -21,6 +21,7 @@ export const AUTOMATION_LIST = defineTool({ }, inputSchema: z.object({ virtual_mcp_id: z.string().optional().nullable(), + search: z.string().optional().nullable(), }), outputSchema: z.object({ automations: z.array( @@ -45,6 +46,7 @@ export const AUTOMATION_LIST = defineTool({ const automations = await ctx.storage.automations.listWithTriggerCounts( organization.id, input.virtual_mcp_id, + input.search, ); const results = automations.map((automation) => { diff --git a/apps/mesh/src/web/hooks/use-automations.ts b/apps/mesh/src/web/hooks/use-automations.ts index 51b6d56fb3..99437c5956 100644 --- a/apps/mesh/src/web/hooks/use-automations.ts +++ b/apps/mesh/src/web/hooks/use-automations.ts @@ -153,7 +153,10 @@ export interface AutomationDetail { type AutomationListOutput = { automations: AutomationListItem[] }; -export function useAutomations(virtualMcpId?: string | null) { +export function useAutomations( + virtualMcpId?: string | null, + search?: string | null, +) { const { org } = useProjectContext(); const client = useMCPClient({ connectionId: SELF_MCP_ALIAS_ID, @@ -161,12 +164,13 @@ export function useAutomations(virtualMcpId?: string | null) { }); return useQuery({ - queryKey: KEYS.automations(org.id, virtualMcpId), + queryKey: KEYS.automations(org.id, virtualMcpId, search), queryFn: async () => { const args: Record = virtualMcpId !== undefined && virtualMcpId !== null ? { virtual_mcp_id: virtualMcpId } : {}; + if (search) args.search = search; const result = (await client.callTool({ name: "AUTOMATION_LIST", arguments: args, diff --git a/apps/mesh/src/web/layouts/tasks-panel/index.tsx b/apps/mesh/src/web/layouts/tasks-panel/index.tsx index 44460dc1e8..69fa93f753 100644 --- a/apps/mesh/src/web/layouts/tasks-panel/index.tsx +++ b/apps/mesh/src/web/layouts/tasks-panel/index.tsx @@ -4,7 +4,7 @@ * Automation-triggered tasks are distinguished by a badge on their avatar. */ -import { Suspense } from "react"; +import { Suspense, useState, useTransition } from "react"; import { useParams } from "@tanstack/react-router"; import { useMCPClient, @@ -23,20 +23,25 @@ import { useTasksAutoRefresh } from "@/web/hooks/use-tasks-auto-refresh"; import { usePanelActions } from "@/web/layouts/shell-layout"; import { KEYS } from "@/web/lib/query-keys"; import { toast } from "sonner"; -import { authClient } from "@/web/lib/auth-client"; -import { TasksSection } from "./tasks-section"; +import { + TasksSection, + type FilterOption, + type MemberFilter, +} from "./tasks-section"; function TasksPanelContent() { useTasksAutoRefresh(); - const { data: session } = authClient.useSession(); - const currentUserId = session?.user?.id; + const [memberFilter, setMemberFilter] = useState("mine"); + const [typeFilter, setTypeFilter] = useState("all"); + const [, startFilterTransition] = useTransition(); + const { tasks: myTasks } = useTasks({ owner: "me", status: "open", hasTrigger: false, }); const { tasks: automationTasks } = useTasks({ - owner: "all", + owner: memberFilter === "mine" ? "me" : "all", status: "open", hasTrigger: true, }); @@ -51,11 +56,21 @@ function TasksPanelContent() { const activeTaskId = params.taskId ?? null; + const taggedAutomationTasks = automationTasks.map((t) => ({ + ...t, + fromAutomation: true as const, + })); + const allTasks = [ - ...myTasks, - ...automationTasks.map((t) => ({ ...t, fromAutomation: true as const })), + ...(typeFilter !== "automation" ? myTasks : []), + ...(typeFilter !== "manual" ? taggedAutomationTasks : []), ].sort((a, b) => (b.updated_at ?? "").localeCompare(a.updated_at ?? "")); + const handleSetMemberFilter = (v: MemberFilter) => + startFilterTransition(() => setMemberFilter(v)); + const handleSetTypeFilter = (v: FilterOption) => + startFilterTransition(() => setTypeFilter(v)); + const handleArchive = async (task: Task) => { try { await callUpdateTaskTool(client, task.id, { hidden: true }); @@ -90,7 +105,10 @@ function TasksPanelContent() { onArchive={handleArchive} onNew={createNewTask} showNewButton - currentUserId={currentUserId} + filter={typeFilter} + setFilter={handleSetTypeFilter} + memberFilter={memberFilter} + setMemberFilter={handleSetMemberFilter} /> ); diff --git a/apps/mesh/src/web/layouts/tasks-panel/tasks-section.tsx b/apps/mesh/src/web/layouts/tasks-panel/tasks-section.tsx index 03ff7c3682..80a71cc9ea 100644 --- a/apps/mesh/src/web/layouts/tasks-panel/tasks-section.tsx +++ b/apps/mesh/src/web/layouts/tasks-panel/tasks-section.tsx @@ -1,4 +1,3 @@ -import { useState } from "react"; import { Edit05, FilterLines, User02, Users03 } from "@untitledui/icons"; import { DropdownMenu, @@ -11,8 +10,8 @@ import { cn } from "@deco/ui/lib/utils.js"; import type { Task } from "@/web/components/chat/task/types"; import { TaskRow } from "./task-row"; -type FilterOption = "all" | "manual" | "automation"; -type MemberFilter = "all" | "mine"; +export type FilterOption = "all" | "manual" | "automation"; +export type MemberFilter = "all" | "mine"; const FILTER_LABELS: Record = { all: "All tasks", @@ -35,7 +34,10 @@ export function TasksSection({ showNewButton, showAutomationBadge, emptyLabel, - currentUserId, + filter, + setFilter, + memberFilter, + setMemberFilter, }: { title: string; tasks: Task[]; @@ -46,22 +48,12 @@ export function TasksSection({ showNewButton?: boolean; showAutomationBadge?: boolean; emptyLabel?: string; - currentUserId?: string; + filter: FilterOption; + setFilter: (v: FilterOption) => void; + memberFilter: MemberFilter; + setMemberFilter: (v: MemberFilter) => void; }) { - const [filter, setFilter] = useState("all"); - const [memberFilter, setMemberFilter] = useState("mine"); - - const memberFiltered = - memberFilter === "mine" && currentUserId - ? tasks.filter((t) => t.created_by === currentUserId) - : tasks; - - const visibleTasks = - filter === "automation" - ? memberFiltered.filter((t) => t.fromAutomation) - : filter === "manual" - ? memberFiltered.filter((t) => !t.fromAutomation) - : memberFiltered; + const visibleTasks = tasks; return (
diff --git a/apps/mesh/src/web/lib/query-keys.ts b/apps/mesh/src/web/lib/query-keys.ts index c490d3cb7c..3ad99fb1d3 100644 --- a/apps/mesh/src/web/lib/query-keys.ts +++ b/apps/mesh/src/web/lib/query-keys.ts @@ -242,8 +242,17 @@ export const KEYS = { // Automations (scoped by organization, optionally by project) automationsAll: (organizationId: string) => ["automations", organizationId] as const, - automations: (organizationId: string, virtualMcpId?: string | null) => - ["automations", organizationId, virtualMcpId ?? null] as const, + automations: ( + organizationId: string, + virtualMcpId?: string | null, + search?: string | null, + ) => + [ + "automations", + organizationId, + virtualMcpId ?? null, + search ?? null, + ] as const, automation: (organizationId: string, id: string) => ["automation", organizationId, id] as const, automationRuns: ( diff --git a/apps/mesh/src/web/views/automations/automations-list.tsx b/apps/mesh/src/web/views/automations/automations-list.tsx index 297a032806..1a62a8567b 100644 --- a/apps/mesh/src/web/views/automations/automations-list.tsx +++ b/apps/mesh/src/web/views/automations/automations-list.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useState, useTransition } from "react"; import { useNavigate } from "@tanstack/react-router"; import { Plus, Zap } from "@untitledui/icons"; import { Button } from "@deco/ui/components/button.tsx"; @@ -14,14 +14,21 @@ import { AutomationListRow } from "./automation-list-row"; export function AutomationsList({ virtualMcpId }: { virtualMcpId: string }) { const navigate = useNavigate(); - const { data: automations = [] } = useAutomations(virtualMcpId); - const { create } = useAutomationActions(); const [search, setSearch] = useState(""); - - const lowerSearch = search.toLowerCase(); - const filtered = automations.filter((a) => - a.name.toLowerCase().includes(lowerSearch), + const [serverSearch, setServerSearch] = useState(""); + const [, startTransition] = useTransition(); + const { data: automations = [] } = useAutomations( + virtualMcpId, + serverSearch || null, ); + const { create } = useAutomationActions(); + + const handleSearch = (value: string) => { + setSearch(value); + startTransition(() => { + setServerSearch(value); + }); + }; const goToDetail = (id: string) => navigate({ @@ -63,7 +70,7 @@ export function AutomationsList({ virtualMcpId }: { virtualMcpId: string }) { {automations.length > 0 && ( @@ -88,7 +95,7 @@ export function AutomationsList({ virtualMcpId }: { virtualMcpId: string }) { } />
- ) : filtered.length === 0 ? ( + ) : automations.length === 0 && serverSearch ? (
} @@ -98,7 +105,7 @@ export function AutomationsList({ virtualMcpId }: { virtualMcpId: string }) {
) : (
- {filtered.map((a) => ( + {automations.map((a) => ( Date: Fri, 24 Apr 2026 09:52:34 -0300 Subject: [PATCH 2/2] fix(tasks-panel): apply member filter to manual tasks query too myTasks was hardcoded to owner:"me" so in "All members" mode manual tasks still only showed the current user's chats. Both queries now share taskOwner derived from memberFilter. Co-Authored-By: Claude Sonnet 4.6 --- apps/mesh/src/web/layouts/tasks-panel/index.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/mesh/src/web/layouts/tasks-panel/index.tsx b/apps/mesh/src/web/layouts/tasks-panel/index.tsx index 69fa93f753..02dc10f929 100644 --- a/apps/mesh/src/web/layouts/tasks-panel/index.tsx +++ b/apps/mesh/src/web/layouts/tasks-panel/index.tsx @@ -35,13 +35,15 @@ function TasksPanelContent() { const [typeFilter, setTypeFilter] = useState("all"); const [, startFilterTransition] = useTransition(); + const taskOwner = memberFilter === "mine" ? "me" : "all"; + const { tasks: myTasks } = useTasks({ - owner: "me", + owner: taskOwner, status: "open", hasTrigger: false, }); const { tasks: automationTasks } = useTasks({ - owner: memberFilter === "mine" ? "me" : "all", + owner: taskOwner, status: "open", hasTrigger: true, });