Use issue participation for agent history
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
5a1e17f27f
commit
02c779b41d
7 changed files with 337 additions and 10 deletions
284
server/src/__tests__/issues-service.test.ts
Normal file
284
server/src/__tests__/issues-service.test.ts
Normal file
|
|
@ -0,0 +1,284 @@
|
||||||
|
import { randomUUID } from "node:crypto";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import net from "node:net";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
activityLog,
|
||||||
|
agents,
|
||||||
|
applyPendingMigrations,
|
||||||
|
companies,
|
||||||
|
createDb,
|
||||||
|
ensurePostgresDatabase,
|
||||||
|
issueComments,
|
||||||
|
issues,
|
||||||
|
} from "@paperclipai/db";
|
||||||
|
import { issueService } from "../services/issues.ts";
|
||||||
|
|
||||||
|
type EmbeddedPostgresInstance = {
|
||||||
|
initialise(): Promise<void>;
|
||||||
|
start(): Promise<void>;
|
||||||
|
stop(): Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type EmbeddedPostgresCtor = new (opts: {
|
||||||
|
databaseDir: string;
|
||||||
|
user: string;
|
||||||
|
password: string;
|
||||||
|
port: number;
|
||||||
|
persistent: boolean;
|
||||||
|
initdbFlags?: string[];
|
||||||
|
onLog?: (message: unknown) => void;
|
||||||
|
onError?: (message: unknown) => void;
|
||||||
|
}) => EmbeddedPostgresInstance;
|
||||||
|
|
||||||
|
async function getEmbeddedPostgresCtor(): Promise<EmbeddedPostgresCtor> {
|
||||||
|
const mod = await import("embedded-postgres");
|
||||||
|
return mod.default as EmbeddedPostgresCtor;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAvailablePort(): Promise<number> {
|
||||||
|
return await new Promise((resolve, reject) => {
|
||||||
|
const server = net.createServer();
|
||||||
|
server.unref();
|
||||||
|
server.on("error", reject);
|
||||||
|
server.listen(0, "127.0.0.1", () => {
|
||||||
|
const address = server.address();
|
||||||
|
if (!address || typeof address === "string") {
|
||||||
|
server.close(() => reject(new Error("Failed to allocate test port")));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { port } = address;
|
||||||
|
server.close((error) => {
|
||||||
|
if (error) reject(error);
|
||||||
|
else resolve(port);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startTempDatabase() {
|
||||||
|
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-issues-service-"));
|
||||||
|
const port = await getAvailablePort();
|
||||||
|
const EmbeddedPostgres = await getEmbeddedPostgresCtor();
|
||||||
|
const instance = new EmbeddedPostgres({
|
||||||
|
databaseDir: dataDir,
|
||||||
|
user: "paperclip",
|
||||||
|
password: "paperclip",
|
||||||
|
port,
|
||||||
|
persistent: true,
|
||||||
|
initdbFlags: ["--encoding=UTF8", "--locale=C"],
|
||||||
|
onLog: () => {},
|
||||||
|
onError: () => {},
|
||||||
|
});
|
||||||
|
await instance.initialise();
|
||||||
|
await instance.start();
|
||||||
|
|
||||||
|
const adminConnectionString = `postgres://paperclip:paperclip@127.0.0.1:${port}/postgres`;
|
||||||
|
await ensurePostgresDatabase(adminConnectionString, "paperclip");
|
||||||
|
const connectionString = `postgres://paperclip:paperclip@127.0.0.1:${port}/paperclip`;
|
||||||
|
await applyPendingMigrations(connectionString);
|
||||||
|
return { connectionString, dataDir, instance };
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("issueService.list participantAgentId", () => {
|
||||||
|
let db!: ReturnType<typeof createDb>;
|
||||||
|
let svc!: ReturnType<typeof issueService>;
|
||||||
|
let instance: EmbeddedPostgresInstance | null = null;
|
||||||
|
let dataDir = "";
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const started = await startTempDatabase();
|
||||||
|
db = createDb(started.connectionString);
|
||||||
|
svc = issueService(db);
|
||||||
|
instance = started.instance;
|
||||||
|
dataDir = started.dataDir;
|
||||||
|
}, 20_000);
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await db.delete(issueComments);
|
||||||
|
await db.delete(activityLog);
|
||||||
|
await db.delete(issues);
|
||||||
|
await db.delete(agents);
|
||||||
|
await db.delete(companies);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await instance?.stop();
|
||||||
|
if (dataDir) {
|
||||||
|
fs.rmSync(dataDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns issues an agent participated in across the supported signals", async () => {
|
||||||
|
const companyId = randomUUID();
|
||||||
|
const agentId = randomUUID();
|
||||||
|
const otherAgentId = randomUUID();
|
||||||
|
|
||||||
|
await db.insert(companies).values({
|
||||||
|
id: companyId,
|
||||||
|
name: "Paperclip",
|
||||||
|
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
||||||
|
requireBoardApprovalForNewAgents: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.insert(agents).values([
|
||||||
|
{
|
||||||
|
id: agentId,
|
||||||
|
companyId,
|
||||||
|
name: "CodexCoder",
|
||||||
|
role: "engineer",
|
||||||
|
status: "active",
|
||||||
|
adapterType: "codex_local",
|
||||||
|
adapterConfig: {},
|
||||||
|
runtimeConfig: {},
|
||||||
|
permissions: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: otherAgentId,
|
||||||
|
companyId,
|
||||||
|
name: "OtherAgent",
|
||||||
|
role: "engineer",
|
||||||
|
status: "active",
|
||||||
|
adapterType: "codex_local",
|
||||||
|
adapterConfig: {},
|
||||||
|
runtimeConfig: {},
|
||||||
|
permissions: {},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const assignedIssueId = randomUUID();
|
||||||
|
const createdIssueId = randomUUID();
|
||||||
|
const commentedIssueId = randomUUID();
|
||||||
|
const activityIssueId = randomUUID();
|
||||||
|
const excludedIssueId = randomUUID();
|
||||||
|
|
||||||
|
await db.insert(issues).values([
|
||||||
|
{
|
||||||
|
id: assignedIssueId,
|
||||||
|
companyId,
|
||||||
|
title: "Assigned issue",
|
||||||
|
status: "todo",
|
||||||
|
priority: "medium",
|
||||||
|
assigneeAgentId: agentId,
|
||||||
|
createdByAgentId: otherAgentId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: createdIssueId,
|
||||||
|
companyId,
|
||||||
|
title: "Created issue",
|
||||||
|
status: "todo",
|
||||||
|
priority: "medium",
|
||||||
|
createdByAgentId: agentId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: commentedIssueId,
|
||||||
|
companyId,
|
||||||
|
title: "Commented issue",
|
||||||
|
status: "todo",
|
||||||
|
priority: "medium",
|
||||||
|
createdByAgentId: otherAgentId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: activityIssueId,
|
||||||
|
companyId,
|
||||||
|
title: "Activity issue",
|
||||||
|
status: "todo",
|
||||||
|
priority: "medium",
|
||||||
|
createdByAgentId: otherAgentId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: excludedIssueId,
|
||||||
|
companyId,
|
||||||
|
title: "Excluded issue",
|
||||||
|
status: "todo",
|
||||||
|
priority: "medium",
|
||||||
|
createdByAgentId: otherAgentId,
|
||||||
|
assigneeAgentId: otherAgentId,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await db.insert(issueComments).values({
|
||||||
|
companyId,
|
||||||
|
issueId: commentedIssueId,
|
||||||
|
authorAgentId: agentId,
|
||||||
|
body: "Investigating this issue.",
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.insert(activityLog).values({
|
||||||
|
companyId,
|
||||||
|
actorType: "agent",
|
||||||
|
actorId: agentId,
|
||||||
|
action: "issue.updated",
|
||||||
|
entityType: "issue",
|
||||||
|
entityId: activityIssueId,
|
||||||
|
agentId,
|
||||||
|
details: { changed: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await svc.list(companyId, { participantAgentId: agentId });
|
||||||
|
const resultIds = new Set(result.map((issue) => issue.id));
|
||||||
|
|
||||||
|
expect(resultIds).toEqual(new Set([
|
||||||
|
assignedIssueId,
|
||||||
|
createdIssueId,
|
||||||
|
commentedIssueId,
|
||||||
|
activityIssueId,
|
||||||
|
]));
|
||||||
|
expect(resultIds.has(excludedIssueId)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("combines participation filtering with search", async () => {
|
||||||
|
const companyId = randomUUID();
|
||||||
|
const agentId = randomUUID();
|
||||||
|
|
||||||
|
await db.insert(companies).values({
|
||||||
|
id: companyId,
|
||||||
|
name: "Paperclip",
|
||||||
|
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
||||||
|
requireBoardApprovalForNewAgents: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.insert(agents).values({
|
||||||
|
id: agentId,
|
||||||
|
companyId,
|
||||||
|
name: "CodexCoder",
|
||||||
|
role: "engineer",
|
||||||
|
status: "active",
|
||||||
|
adapterType: "codex_local",
|
||||||
|
adapterConfig: {},
|
||||||
|
runtimeConfig: {},
|
||||||
|
permissions: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const matchedIssueId = randomUUID();
|
||||||
|
const otherIssueId = randomUUID();
|
||||||
|
|
||||||
|
await db.insert(issues).values([
|
||||||
|
{
|
||||||
|
id: matchedIssueId,
|
||||||
|
companyId,
|
||||||
|
title: "Invoice reconciliation",
|
||||||
|
status: "todo",
|
||||||
|
priority: "medium",
|
||||||
|
createdByAgentId: agentId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: otherIssueId,
|
||||||
|
companyId,
|
||||||
|
title: "Weekly planning",
|
||||||
|
status: "todo",
|
||||||
|
priority: "medium",
|
||||||
|
createdByAgentId: agentId,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = await svc.list(companyId, {
|
||||||
|
participantAgentId: agentId,
|
||||||
|
q: "invoice",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.map((issue) => issue.id)).toEqual([matchedIssueId]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -233,6 +233,7 @@ export function issueRoutes(db: Db, storage: StorageService) {
|
||||||
const result = await svc.list(companyId, {
|
const result = await svc.list(companyId, {
|
||||||
status: req.query.status as string | undefined,
|
status: req.query.status as string | undefined,
|
||||||
assigneeAgentId: req.query.assigneeAgentId as string | undefined,
|
assigneeAgentId: req.query.assigneeAgentId as string | undefined,
|
||||||
|
participantAgentId: req.query.participantAgentId as string | undefined,
|
||||||
assigneeUserId,
|
assigneeUserId,
|
||||||
touchedByUserId,
|
touchedByUserId,
|
||||||
unreadForUserId,
|
unreadForUserId,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { and, asc, desc, eq, inArray, isNull, ne, or, sql } from "drizzle-orm";
|
import { and, asc, desc, eq, inArray, isNull, ne, or, sql } from "drizzle-orm";
|
||||||
import type { Db } from "@paperclipai/db";
|
import type { Db } from "@paperclipai/db";
|
||||||
import {
|
import {
|
||||||
|
activityLog,
|
||||||
agents,
|
agents,
|
||||||
assets,
|
assets,
|
||||||
companies,
|
companies,
|
||||||
|
|
@ -62,6 +63,7 @@ function applyStatusSideEffects(
|
||||||
export interface IssueFilters {
|
export interface IssueFilters {
|
||||||
status?: string;
|
status?: string;
|
||||||
assigneeAgentId?: string;
|
assigneeAgentId?: string;
|
||||||
|
participantAgentId?: string;
|
||||||
assigneeUserId?: string;
|
assigneeUserId?: string;
|
||||||
touchedByUserId?: string;
|
touchedByUserId?: string;
|
||||||
unreadForUserId?: string;
|
unreadForUserId?: string;
|
||||||
|
|
@ -134,6 +136,30 @@ function touchedByUserCondition(companyId: string, userId: string) {
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function participatedByAgentCondition(companyId: string, agentId: string) {
|
||||||
|
return sql<boolean>`
|
||||||
|
(
|
||||||
|
${issues.createdByAgentId} = ${agentId}
|
||||||
|
OR ${issues.assigneeAgentId} = ${agentId}
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM ${issueComments}
|
||||||
|
WHERE ${issueComments.issueId} = ${issues.id}
|
||||||
|
AND ${issueComments.companyId} = ${companyId}
|
||||||
|
AND ${issueComments.authorAgentId} = ${agentId}
|
||||||
|
)
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM ${activityLog}
|
||||||
|
WHERE ${activityLog.companyId} = ${companyId}
|
||||||
|
AND ${activityLog.entityType} = 'issue'
|
||||||
|
AND ${activityLog.entityId} = ${issues.id}::text
|
||||||
|
AND ${activityLog.agentId} = ${agentId}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
function myLastCommentAtExpr(companyId: string, userId: string) {
|
function myLastCommentAtExpr(companyId: string, userId: string) {
|
||||||
return sql<Date | null>`
|
return sql<Date | null>`
|
||||||
(
|
(
|
||||||
|
|
@ -508,6 +534,9 @@ export function issueService(db: Db) {
|
||||||
if (filters?.assigneeAgentId) {
|
if (filters?.assigneeAgentId) {
|
||||||
conditions.push(eq(issues.assigneeAgentId, filters.assigneeAgentId));
|
conditions.push(eq(issues.assigneeAgentId, filters.assigneeAgentId));
|
||||||
}
|
}
|
||||||
|
if (filters?.participantAgentId) {
|
||||||
|
conditions.push(participatedByAgentCondition(companyId, filters.participantAgentId));
|
||||||
|
}
|
||||||
if (filters?.assigneeUserId) {
|
if (filters?.assigneeUserId) {
|
||||||
conditions.push(eq(issues.assigneeUserId, filters.assigneeUserId));
|
conditions.push(eq(issues.assigneeUserId, filters.assigneeUserId));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ export const issuesApi = {
|
||||||
status?: string;
|
status?: string;
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
assigneeAgentId?: string;
|
assigneeAgentId?: string;
|
||||||
|
participantAgentId?: string;
|
||||||
assigneeUserId?: string;
|
assigneeUserId?: string;
|
||||||
touchedByUserId?: string;
|
touchedByUserId?: string;
|
||||||
unreadForUserId?: string;
|
unreadForUserId?: string;
|
||||||
|
|
@ -32,6 +33,7 @@ export const issuesApi = {
|
||||||
if (filters?.status) params.set("status", filters.status);
|
if (filters?.status) params.set("status", filters.status);
|
||||||
if (filters?.projectId) params.set("projectId", filters.projectId);
|
if (filters?.projectId) params.set("projectId", filters.projectId);
|
||||||
if (filters?.assigneeAgentId) params.set("assigneeAgentId", filters.assigneeAgentId);
|
if (filters?.assigneeAgentId) params.set("assigneeAgentId", filters.assigneeAgentId);
|
||||||
|
if (filters?.participantAgentId) params.set("participantAgentId", filters.participantAgentId);
|
||||||
if (filters?.assigneeUserId) params.set("assigneeUserId", filters.assigneeUserId);
|
if (filters?.assigneeUserId) params.set("assigneeUserId", filters.assigneeUserId);
|
||||||
if (filters?.touchedByUserId) params.set("touchedByUserId", filters.touchedByUserId);
|
if (filters?.touchedByUserId) params.set("touchedByUserId", filters.touchedByUserId);
|
||||||
if (filters?.unreadForUserId) params.set("unreadForUserId", filters.unreadForUserId);
|
if (filters?.unreadForUserId) params.set("unreadForUserId", filters.unreadForUserId);
|
||||||
|
|
|
||||||
|
|
@ -166,6 +166,9 @@ interface IssuesListProps {
|
||||||
issueLinkState?: unknown;
|
issueLinkState?: unknown;
|
||||||
initialAssignees?: string[];
|
initialAssignees?: string[];
|
||||||
initialSearch?: string;
|
initialSearch?: string;
|
||||||
|
searchFilters?: {
|
||||||
|
participantAgentId?: string;
|
||||||
|
};
|
||||||
onSearchChange?: (search: string) => void;
|
onSearchChange?: (search: string) => void;
|
||||||
onUpdateIssue: (id: string, data: Record<string, unknown>) => void;
|
onUpdateIssue: (id: string, data: Record<string, unknown>) => void;
|
||||||
}
|
}
|
||||||
|
|
@ -182,6 +185,7 @@ export function IssuesList({
|
||||||
issueLinkState,
|
issueLinkState,
|
||||||
initialAssignees,
|
initialAssignees,
|
||||||
initialSearch,
|
initialSearch,
|
||||||
|
searchFilters,
|
||||||
onSearchChange,
|
onSearchChange,
|
||||||
onUpdateIssue,
|
onUpdateIssue,
|
||||||
}: IssuesListProps) {
|
}: IssuesListProps) {
|
||||||
|
|
@ -239,8 +243,11 @@ export function IssuesList({
|
||||||
}, [scopedKey]);
|
}, [scopedKey]);
|
||||||
|
|
||||||
const { data: searchedIssues = [] } = useQuery({
|
const { data: searchedIssues = [] } = useQuery({
|
||||||
queryKey: queryKeys.issues.search(selectedCompanyId!, normalizedIssueSearch, projectId),
|
queryKey: [
|
||||||
queryFn: () => issuesApi.list(selectedCompanyId!, { q: normalizedIssueSearch, projectId }),
|
...queryKeys.issues.search(selectedCompanyId!, normalizedIssueSearch, projectId),
|
||||||
|
searchFilters ?? {},
|
||||||
|
],
|
||||||
|
queryFn: () => issuesApi.list(selectedCompanyId!, { q: normalizedIssueSearch, projectId, ...searchFilters }),
|
||||||
enabled: !!selectedCompanyId && normalizedIssueSearch.length > 0,
|
enabled: !!selectedCompanyId && normalizedIssueSearch.length > 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -572,9 +572,9 @@ export function AgentDetail() {
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: allIssues } = useQuery({
|
const { data: allIssues } = useQuery({
|
||||||
queryKey: queryKeys.issues.list(resolvedCompanyId!),
|
queryKey: [...queryKeys.issues.list(resolvedCompanyId!), "participant-agent", resolvedAgentId ?? "__none__"],
|
||||||
queryFn: () => issuesApi.list(resolvedCompanyId!),
|
queryFn: () => issuesApi.list(resolvedCompanyId!, { participantAgentId: resolvedAgentId! }),
|
||||||
enabled: !!resolvedCompanyId && needsDashboardData,
|
enabled: !!resolvedCompanyId && !!resolvedAgentId && needsDashboardData,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: allAgents } = useQuery({
|
const { data: allAgents } = useQuery({
|
||||||
|
|
@ -592,7 +592,6 @@ export function AgentDetail() {
|
||||||
});
|
});
|
||||||
|
|
||||||
const assignedIssues = (allIssues ?? [])
|
const assignedIssues = (allIssues ?? [])
|
||||||
.filter((i) => i.assigneeAgentId === agent?.id)
|
|
||||||
.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
|
.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
|
||||||
const reportsToAgent = (allAgents ?? []).find((a) => a.id === agent?.reportsTo);
|
const reportsToAgent = (allAgents ?? []).find((a) => a.id === agent?.reportsTo);
|
||||||
const directReports = (allAgents ?? []).filter((a) => a.reportsTo === agent?.id && a.status !== "terminated");
|
const directReports = (allAgents ?? []).filter((a) => a.reportsTo === agent?.id && a.status !== "terminated");
|
||||||
|
|
@ -1174,12 +1173,15 @@ function AgentOverview({
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h3 className="text-sm font-medium">Recent Issues</h3>
|
<h3 className="text-sm font-medium">Recent Issues</h3>
|
||||||
<Link to={`/issues?assignee=${agentId}`} className="text-xs text-muted-foreground hover:text-foreground transition-colors">
|
<Link
|
||||||
|
to={`/issues?participantAgentId=${agentId}`}
|
||||||
|
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
See All →
|
See All →
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
{assignedIssues.length === 0 ? (
|
{assignedIssues.length === 0 ? (
|
||||||
<p className="text-sm text-muted-foreground">No assigned issues.</p>
|
<p className="text-sm text-muted-foreground">No recent issues.</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="border border-border rounded-lg">
|
<div className="border border-border rounded-lg">
|
||||||
{assignedIssues.slice(0, 10).map((issue) => (
|
{assignedIssues.slice(0, 10).map((issue) => (
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ export function Issues() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const initialSearch = searchParams.get("q") ?? "";
|
const initialSearch = searchParams.get("q") ?? "";
|
||||||
|
const participantAgentId = searchParams.get("participantAgentId") ?? undefined;
|
||||||
const debounceRef = useRef<ReturnType<typeof setTimeout>>(undefined);
|
const debounceRef = useRef<ReturnType<typeof setTimeout>>(undefined);
|
||||||
const handleSearchChange = useCallback((search: string) => {
|
const handleSearchChange = useCallback((search: string) => {
|
||||||
clearTimeout(debounceRef.current);
|
clearTimeout(debounceRef.current);
|
||||||
|
|
@ -86,8 +87,8 @@ export function Issues() {
|
||||||
}, [setBreadcrumbs]);
|
}, [setBreadcrumbs]);
|
||||||
|
|
||||||
const { data: issues, isLoading, error } = useQuery({
|
const { data: issues, isLoading, error } = useQuery({
|
||||||
queryKey: queryKeys.issues.list(selectedCompanyId!),
|
queryKey: [...queryKeys.issues.list(selectedCompanyId!), "participant-agent", participantAgentId ?? "__all__"],
|
||||||
queryFn: () => issuesApi.list(selectedCompanyId!),
|
queryFn: () => issuesApi.list(selectedCompanyId!, { participantAgentId }),
|
||||||
enabled: !!selectedCompanyId,
|
enabled: !!selectedCompanyId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -117,6 +118,7 @@ export function Issues() {
|
||||||
initialSearch={initialSearch}
|
initialSearch={initialSearch}
|
||||||
onSearchChange={handleSearchChange}
|
onSearchChange={handleSearchChange}
|
||||||
onUpdateIssue={(id, data) => updateIssue.mutate({ id, data })}
|
onUpdateIssue={(id, data) => updateIssue.mutate({ id, data })}
|
||||||
|
searchFilters={participantAgentId ? { participantAgentId } : undefined}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue