fix(tools): OR'ed where clauses now AND'd so search excludes soft-deleted tools; localize delete confirm dialog
Build & Push Docker Image / build (push) Successful in 2m25s

This commit is contained in:
opencode
2026-08-03 08:48:08 +02:00
parent 743b177c89
commit f851305d78
4 changed files with 25 additions and 13 deletions
+8 -6
View File
@@ -1,5 +1,5 @@
import { Router, type IRouter } from "express"; import { Router, type IRouter } from "express";
import { eq, desc, asc, sql, and, not, isNull, inArray } from "drizzle-orm"; import { eq, desc, asc, sql, and, not, isNull, inArray, type SQL } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { db, toolsTable, ratingsTable, toolRelationsTable } from "@workspace/db"; import { db, toolsTable, ratingsTable, toolRelationsTable } from "@workspace/db";
import { import {
@@ -57,21 +57,23 @@ router.get("/tools", async (req, res): Promise<void> => {
const tagList = (tags ?? "").split(",").map((t) => t.trim()).filter(Boolean); const tagList = (tags ?? "").split(",").map((t) => t.trim()).filter(Boolean);
const featureList = (features ?? "").split(",").map((f) => f.trim()).filter(Boolean); const featureList = (features ?? "").split(",").map((f) => f.trim()).filter(Boolean);
let query = db.select().from(toolsTable).where(isNull(toolsTable.deletedAt)).$dynamic(); const conditions: SQL[] = [isNull(toolsTable.deletedAt)];
if (category) { if (category) {
query = query.where(eq(toolsTable.category, category)); conditions.push(eq(toolsTable.category, category));
} }
if (search) { if (search) {
const escaped = search.replace(/[%_\\]/g, (m) => `\\${m}`); const escaped = search.replace(/[%_\\]/g, (m) => `\\${m}`);
query = query.where(sql`${toolsTable.name} ilike ${`%${escaped}%`} escape '\\'`); conditions.push(sql`${toolsTable.name} ilike ${`%${escaped}%`} escape '\\'`);
} }
if (tagList.length > 0) { if (tagList.length > 0) {
query = query.where(sql`${toolsTable.tags} @> ARRAY[${sql.join(tagList.map((t) => sql`${t}`), sql`, `)}]::text[]`); conditions.push(sql`${toolsTable.tags} @> ARRAY[${sql.join(tagList.map((t) => sql`${t}`), sql`, `)}]::text[]`);
} }
if (featureList.length > 0) { if (featureList.length > 0) {
query = query.where(sql`${toolsTable.features} @> ARRAY[${sql.join(featureList.map((f) => sql`${f}`), sql`, `)}]::text[]`); conditions.push(sql`${toolsTable.features} @> ARRAY[${sql.join(featureList.map((f) => sql`${f}`), sql`, `)}]::text[]`);
} }
const query = db.select().from(toolsTable).where(and(...conditions));
const tools = await query.orderBy(desc(toolsTable.createdAt)); const tools = await query.orderBy(desc(toolsTable.createdAt));
const toolIds = tools.map((t) => t.id); const toolIds = tools.map((t) => t.id);
+6 -1
View File
@@ -127,7 +127,12 @@
"relatedTools": "Ähnliche Tools", "relatedTools": "Ähnliche Tools",
"costs": "Kosten", "costs": "Kosten",
"addCost": "Kosten hinzufügen", "addCost": "Kosten hinzufügen",
"recentRatings": "Letzte Bewertungen" "recentRatings": "Letzte Bewertungen",
"deleteConfirmTitle": "Dieses Tool löschen?",
"deleteToTrash": "Dies verschiebt {{name}} in den Papierkorb. Es kann später wiederhergestellt werden.",
"deletePermanent": "Dies entfernt {{name}} dauerhaft inklusive aller Bewertungen. Das kann nicht rückgängig gemacht werden.",
"deleting": "Löschen…",
"deleteAction": "Löschen"
}, },
"compare": { "compare": {
"title": "Tools vergleichen", "title": "Tools vergleichen",
+6 -1
View File
@@ -127,7 +127,12 @@
"relatedTools": "Related Tools", "relatedTools": "Related Tools",
"costs": "Costs", "costs": "Costs",
"addCost": "Add Cost", "addCost": "Add Cost",
"recentRatings": "Recent Ratings" "recentRatings": "Recent Ratings",
"deleteConfirmTitle": "Delete this tool?",
"deleteToTrash": "This will move {{name}} to the trash. It can be restored later.",
"deletePermanent": "This will permanently remove {{name}} and all its ratings. This cannot be undone.",
"deleting": "Deleting…",
"deleteAction": "Delete"
}, },
"compare": { "compare": {
"title": "Compare Tools", "title": "Compare Tools",
+5 -5
View File
@@ -914,23 +914,23 @@ export default function ToolDetail() {
<AlertDialog open={deleteOpen} onOpenChange={setDeleteOpen}> <AlertDialog open={deleteOpen} onOpenChange={setDeleteOpen}>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>Delete this tool?</AlertDialogTitle> <AlertDialogTitle>{t("detail.deleteConfirmTitle")}</AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
{hasTrash ? ( {hasTrash ? (
<>This will move <span className="font-medium">{tool?.name}</span> to the trash. It can be restored later.</> <>{t("detail.deleteToTrash", { name: tool?.name })}</>
) : ( ) : (
<>This will permanently remove <span className="font-medium">{tool?.name}</span> and all its ratings. This cannot be undone.</> <>{t("detail.deletePermanent", { name: tool?.name })}</>
)} )}
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel> <AlertDialogCancel>{t("common.cancel")}</AlertDialogCancel>
<AlertDialogAction <AlertDialogAction
className="bg-destructive text-destructive-foreground hover:bg-destructive/90" className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
onClick={handleDelete} onClick={handleDelete}
disabled={deleteTool.isPending} disabled={deleteTool.isPending}
> >
{deleteTool.isPending ? "Deleting" : "Delete"} {deleteTool.isPending ? t("detail.deleting") : t("detail.deleteAction")}
</AlertDialogAction> </AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>