diff options
| author | Mohamed Bassem <me@mbassem.com> | 2024-12-31 14:15:02 +0000 |
|---|---|---|
| committer | Mohamed Bassem <me@mbassem.com> | 2024-12-31 14:20:48 +0000 |
| commit | 96cc11ef321b4580430c05f9344c6eb7dbddcf23 (patch) | |
| tree | 9d7115ff76a2c173794a9b8ce3a54463ab748145 | |
| parent | 4deda9d477141e864f472ba95003e3974346f10d (diff) | |
| download | karakeep-96cc11ef321b4580430c05f9344c6eb7dbddcf23.tar.zst | |
feat: Add support for searching for tagged and listed items
| -rw-r--r-- | apps/web/components/dashboard/search/QueryExplainerTooltip.tsx | 14 | ||||
| -rw-r--r-- | packages/shared/searchQueryParser.test.ts | 32 | ||||
| -rw-r--r-- | packages/shared/searchQueryParser.ts | 10 | ||||
| -rw-r--r-- | packages/shared/types/search.ts | 14 | ||||
| -rw-r--r-- | packages/trpc/lib/__tests__/search.test.ts | 24 | ||||
| -rw-r--r-- | packages/trpc/lib/search.ts | 34 |
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 }) |
