aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMohamed Bassem <me@mbassem.com>2024-12-31 14:15:02 +0000
committerMohamed Bassem <me@mbassem.com>2024-12-31 14:20:48 +0000
commit96cc11ef321b4580430c05f9344c6eb7dbddcf23 (patch)
tree9d7115ff76a2c173794a9b8ce3a54463ab748145
parent4deda9d477141e864f472ba95003e3974346f10d (diff)
downloadkarakeep-96cc11ef321b4580430c05f9344c6eb7dbddcf23.tar.zst
feat: Add support for searching for tagged and listed items
-rw-r--r--apps/web/components/dashboard/search/QueryExplainerTooltip.tsx14
-rw-r--r--packages/shared/searchQueryParser.test.ts32
-rw-r--r--packages/shared/searchQueryParser.ts10
-rw-r--r--packages/shared/types/search.ts14
-rw-r--r--packages/trpc/lib/__tests__/search.test.ts24
-rw-r--r--packages/trpc/lib/search.ts34
6 files changed, 128 insertions, 0 deletions
diff --git a/apps/web/components/dashboard/search/QueryExplainerTooltip.tsx b/apps/web/components/dashboard/search/QueryExplainerTooltip.tsx
index 0a325031..eb7282d0 100644
--- a/apps/web/components/dashboard/search/QueryExplainerTooltip.tsx
+++ b/apps/web/components/dashboard/search/QueryExplainerTooltip.tsx
@@ -63,6 +63,20 @@ export default function QueryExplainerTooltip({
<TableCell>{matcher.archived.toString()}</TableCell>
</TableRow>
);
+ case "tagged":
+ return (
+ <TableRow>
+ <TableCell>Has Tags</TableCell>
+ <TableCell>{matcher.tagged.toString()}</TableCell>
+ </TableRow>
+ );
+ case "inlist":
+ return (
+ <TableRow>
+ <TableCell>In Any List</TableCell>
+ <TableCell>{matcher.inList.toString()}</TableCell>
+ </TableRow>
+ );
case "and":
case "or":
return (
diff --git a/packages/shared/searchQueryParser.test.ts b/packages/shared/searchQueryParser.test.ts
index 5bbb3f77..5af7ca2f 100644
--- a/packages/shared/searchQueryParser.test.ts
+++ b/packages/shared/searchQueryParser.test.ts
@@ -36,6 +36,38 @@ describe("Search Query Parser", () => {
favourited: false,
},
});
+ expect(parseSearchQuery("is:tagged")).toEqual({
+ result: "full",
+ text: "",
+ matcher: {
+ type: "tagged",
+ tagged: true,
+ },
+ });
+ expect(parseSearchQuery("-is:tagged")).toEqual({
+ result: "full",
+ text: "",
+ matcher: {
+ type: "tagged",
+ tagged: false,
+ },
+ });
+ expect(parseSearchQuery("is:inlist")).toEqual({
+ result: "full",
+ text: "",
+ matcher: {
+ type: "inlist",
+ inList: true,
+ },
+ });
+ expect(parseSearchQuery("-is:inlist")).toEqual({
+ result: "full",
+ text: "",
+ matcher: {
+ type: "inlist",
+ inList: false,
+ },
+ });
});
test("simple string queries", () => {
diff --git a/packages/shared/searchQueryParser.ts b/packages/shared/searchQueryParser.ts
index 02129c14..e52af274 100644
--- a/packages/shared/searchQueryParser.ts
+++ b/packages/shared/searchQueryParser.ts
@@ -126,6 +126,16 @@ MATCHER.setPattern(
text: "",
matcher: { type: "archived", archived: !minus },
};
+ case "tagged":
+ return {
+ text: "",
+ matcher: { type: "tagged", tagged: !minus },
+ };
+ case "inlist":
+ return {
+ text: "",
+ matcher: { type: "inlist", inList: !minus },
+ };
default:
// If the token is not known, emit it as pure text
return {
diff --git a/packages/shared/types/search.ts b/packages/shared/types/search.ts
index 4d947a05..9d97fdd8 100644
--- a/packages/shared/types/search.ts
+++ b/packages/shared/types/search.ts
@@ -40,6 +40,16 @@ const zDateBeforeMatcher = z.object({
inverse: z.boolean(),
});
+const zIsTaggedMatcher = z.object({
+ type: z.literal("tagged"),
+ tagged: z.boolean(),
+});
+
+const zIsInListMatcher = z.object({
+ type: z.literal("inlist"),
+ inList: z.boolean(),
+});
+
const zNonRecursiveMatcher = z.union([
zTagNameMatcher,
zListNameMatcher,
@@ -48,6 +58,8 @@ const zNonRecursiveMatcher = z.union([
zFavouritedMatcher,
zDateAfterMatcher,
zDateBeforeMatcher,
+ zIsTaggedMatcher,
+ zIsInListMatcher,
]);
type NonRecursiveMatcher = z.infer<typeof zNonRecursiveMatcher>;
@@ -65,6 +77,8 @@ export const zMatcherSchema: z.ZodType<Matcher> = z.lazy(() => {
zFavouritedMatcher,
zDateAfterMatcher,
zDateBeforeMatcher,
+ zIsTaggedMatcher,
+ zIsInListMatcher,
z.object({
type: z.literal("and"),
matchers: z.array(zMatcherSchema),
diff --git a/packages/trpc/lib/__tests__/search.test.ts b/packages/trpc/lib/__tests__/search.test.ts
index 31f87dfd..bf32bcb1 100644
--- a/packages/trpc/lib/__tests__/search.test.ts
+++ b/packages/trpc/lib/__tests__/search.test.ts
@@ -344,6 +344,30 @@ describe("getBookmarkIdsFromMatcher", () => {
expect(result).toEqual(["b4"]);
});
+ it("should handle tagged matcher", async () => {
+ const matcher: Matcher = { type: "tagged", tagged: true };
+ const result = await getBookmarkIdsFromMatcher(mockCtx, matcher);
+ expect(result.sort()).toEqual(["b1", "b2", "b4", "b5", "b6"]);
+ });
+
+ it("should handle tagged matcher with tagged=false", async () => {
+ const matcher: Matcher = { type: "tagged", tagged: false };
+ const result = await getBookmarkIdsFromMatcher(mockCtx, matcher);
+ expect(result).toEqual(["b3"]);
+ });
+
+ it("should handle inlist matcher", async () => {
+ const matcher: Matcher = { type: "inlist", inList: true };
+ const result = await getBookmarkIdsFromMatcher(mockCtx, matcher);
+ expect(result.sort()).toEqual(["b1", "b2", "b4", "b5", "b6"]);
+ });
+
+ it("should handle inlist matcher with inList=false", async () => {
+ const matcher: Matcher = { type: "inlist", inList: false };
+ const result = await getBookmarkIdsFromMatcher(mockCtx, matcher);
+ expect(result).toEqual(["b3"]);
+ });
+
it("should throw error for unknown matcher type", async () => {
const matcher = { type: "unknown" } as unknown as Matcher;
await expect(getBookmarkIdsFromMatcher(mockCtx, matcher)).rejects.toThrow(
diff --git a/packages/trpc/lib/search.ts b/packages/trpc/lib/search.ts
index fcc5abda..e7e6b5f7 100644
--- a/packages/trpc/lib/search.ts
+++ b/packages/trpc/lib/search.ts
@@ -113,6 +113,23 @@ async function getIds(
),
);
}
+ case "tagged": {
+ const comp = matcher.tagged ? exists : notExists;
+ return db
+ .select({ id: bookmarks.id })
+ .from(bookmarks)
+ .where(
+ and(
+ eq(bookmarks.userId, userId),
+ comp(
+ db
+ .select()
+ .from(tagsOnBookmarks)
+ .where(and(eq(tagsOnBookmarks.bookmarkId, bookmarks.id))),
+ ),
+ ),
+ );
+ }
case "listName": {
const comp = matcher.inverse ? notExists : exists;
return db
@@ -140,6 +157,23 @@ async function getIds(
),
);
}
+ case "inlist": {
+ const comp = matcher.inList ? exists : notExists;
+ return db
+ .select({ id: bookmarks.id })
+ .from(bookmarks)
+ .where(
+ and(
+ eq(bookmarks.userId, userId),
+ comp(
+ db
+ .select()
+ .from(bookmarksInLists)
+ .where(and(eq(bookmarksInLists.bookmarkId, bookmarks.id))),
+ ),
+ ),
+ );
+ }
case "archived": {
return db
.select({ id: bookmarks.id })