fix(issues): support hidden issue flows and filter hidden activity
This commit is contained in:
@@ -5,7 +5,7 @@ import { HttpError } from "../errors.js";
|
|||||||
|
|
||||||
export function errorHandler(
|
export function errorHandler(
|
||||||
err: unknown,
|
err: unknown,
|
||||||
_req: Request,
|
req: Request,
|
||||||
res: Response,
|
res: Response,
|
||||||
_next: NextFunction,
|
_next: NextFunction,
|
||||||
) {
|
) {
|
||||||
@@ -22,6 +22,16 @@ export function errorHandler(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.error(err, "Unhandled error");
|
const errObj = err instanceof Error
|
||||||
|
? { message: err.message, stack: err.stack, name: err.name }
|
||||||
|
: { raw: err };
|
||||||
|
|
||||||
|
logger.error(
|
||||||
|
{ err: errObj, method: req.method, url: req.originalUrl },
|
||||||
|
"Unhandled error: %s %s — %s",
|
||||||
|
req.method,
|
||||||
|
req.originalUrl,
|
||||||
|
err instanceof Error ? err.message : String(err),
|
||||||
|
);
|
||||||
res.status(500).json({ error: "Internal server error" });
|
res.status(500).json({ error: "Internal server error" });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export const logger = pino({
|
|||||||
targets: [
|
targets: [
|
||||||
{
|
{
|
||||||
target: "pino-pretty",
|
target: "pino-pretty",
|
||||||
options: { ...sharedOpts, ignore: "pid,hostname,req,res", hideObject: true, colorize: true, destination: 1 },
|
options: { ...sharedOpts, ignore: "pid,hostname,req,res", colorize: true, destination: 1 },
|
||||||
level: "info",
|
level: "info",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -186,7 +186,10 @@ export function issueRoutes(db: Db) {
|
|||||||
}
|
}
|
||||||
assertCompanyAccess(req, existing.companyId);
|
assertCompanyAccess(req, existing.companyId);
|
||||||
|
|
||||||
const { comment: commentBody, ...updateFields } = req.body;
|
const { comment: commentBody, hiddenAt: hiddenAtRaw, ...updateFields } = req.body;
|
||||||
|
if (hiddenAtRaw !== undefined) {
|
||||||
|
updateFields.hiddenAt = hiddenAtRaw ? new Date(hiddenAtRaw) : null;
|
||||||
|
}
|
||||||
const issue = await svc.update(id, updateFields);
|
const issue = await svc.update(id, updateFields);
|
||||||
if (!issue) {
|
if (!issue) {
|
||||||
res.status(404).json({ error: "Issue not found" });
|
res.status(404).json({ error: "Issue not found" });
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { and, desc, eq, isNotNull, sql } from "drizzle-orm";
|
import { and, desc, eq, isNotNull, isNull, or, sql } from "drizzle-orm";
|
||||||
import type { Db } from "@paperclip/db";
|
import type { Db } from "@paperclip/db";
|
||||||
import { activityLog, heartbeatRuns, issues } from "@paperclip/db";
|
import { activityLog, heartbeatRuns, issues } from "@paperclip/db";
|
||||||
|
|
||||||
@@ -25,7 +25,27 @@ export function activityService(db: Db) {
|
|||||||
conditions.push(eq(activityLog.entityId, filters.entityId));
|
conditions.push(eq(activityLog.entityId, filters.entityId));
|
||||||
}
|
}
|
||||||
|
|
||||||
return db.select().from(activityLog).where(and(...conditions)).orderBy(desc(activityLog.createdAt));
|
return db
|
||||||
|
.select({ activityLog })
|
||||||
|
.from(activityLog)
|
||||||
|
.leftJoin(
|
||||||
|
issues,
|
||||||
|
and(
|
||||||
|
eq(activityLog.entityType, sql`'issue'`),
|
||||||
|
eq(activityLog.entityId, issueIdAsText),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
...conditions,
|
||||||
|
or(
|
||||||
|
sql`${activityLog.entityType} != 'issue'`,
|
||||||
|
isNull(issues.hiddenAt),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.orderBy(desc(activityLog.createdAt))
|
||||||
|
.then((rows) => rows.map((r) => r.activityLog));
|
||||||
},
|
},
|
||||||
|
|
||||||
forIssue: (issueId: string) =>
|
forIssue: (issueId: string) =>
|
||||||
@@ -76,6 +96,7 @@ export function activityService(db: Db) {
|
|||||||
and(
|
and(
|
||||||
eq(activityLog.runId, runId),
|
eq(activityLog.runId, runId),
|
||||||
eq(activityLog.entityType, "issue"),
|
eq(activityLog.entityType, "issue"),
|
||||||
|
isNull(issues.hiddenAt),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.orderBy(issueIdAsText),
|
.orderBy(issueIdAsText),
|
||||||
|
|||||||
@@ -324,6 +324,7 @@ export function issueService(db: Db) {
|
|||||||
and(
|
and(
|
||||||
eq(issues.companyId, companyId),
|
eq(issues.companyId, companyId),
|
||||||
eq(issues.status, "in_progress"),
|
eq(issues.status, "in_progress"),
|
||||||
|
isNull(issues.hiddenAt),
|
||||||
sql`${issues.startedAt} < ${cutoff.toISOString()}`,
|
sql`${issues.startedAt} < ${cutoff.toISOString()}`,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -235,6 +235,13 @@ export function IssueDetail() {
|
|||||||
</nav>
|
</nav>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{issue.hiddenAt && (
|
||||||
|
<div className="flex items-center gap-2 rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive">
|
||||||
|
<EyeOff className="h-4 w-4 shrink-0" />
|
||||||
|
This issue is hidden
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<StatusIcon
|
<StatusIcon
|
||||||
@@ -249,7 +256,7 @@ export function IssueDetail() {
|
|||||||
|
|
||||||
<Popover open={moreOpen} onOpenChange={setMoreOpen}>
|
<Popover open={moreOpen} onOpenChange={setMoreOpen}>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button variant="ghost" size="icon-xs">
|
<Button variant="ghost" size="icon-xs" className="ml-auto">
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
@@ -259,7 +266,7 @@ export function IssueDetail() {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
updateIssue.mutate(
|
updateIssue.mutate(
|
||||||
{ hiddenAt: new Date().toISOString() },
|
{ hiddenAt: new Date().toISOString() },
|
||||||
{ onSuccess: () => navigate("/issues") },
|
{ onSuccess: () => navigate("/issues/all") },
|
||||||
);
|
);
|
||||||
setMoreOpen(false);
|
setMoreOpen(false);
|
||||||
}}
|
}}
|
||||||
|
|||||||
Reference in New Issue
Block a user