Skip to content

Commit

Permalink
feat: Add ability to filter by bookmark type
Browse files Browse the repository at this point in the history
  • Loading branch information
MohamedBassem committed Jan 12, 2025
1 parent c5298cf commit 9fd26b4
Show file tree
Hide file tree
Showing 9 changed files with 208 additions and 14 deletions.
16 changes: 16 additions & 0 deletions apps/web/components/dashboard/search/QueryExplainerTooltip.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import InfoTooltip from "@/components/ui/info-tooltip";
import { Table, TableBody, TableCell, TableRow } from "@/components/ui/table";
import { useTranslation } from "@/lib/i18n/client";
import { match } from "@/lib/utils";

import { TextAndMatcher } from "@hoarder/shared/searchQueryParser";
import { Matcher } from "@hoarder/shared/types/search";
Expand Down Expand Up @@ -134,6 +135,21 @@ export default function QueryExplainerTooltip({
<TableCell>{matcher.url}</TableCell>
</TableRow>
);
case "type":
return (
<TableRow>
<TableCell>
{matcher.inverse ? t("search.type_is_not") : t("search.type_is")}
</TableCell>
<TableCell>
{match(matcher.typeName, {
link: t("common.bookmark_types.link"),
text: t("common.bookmark_types.text"),
asset: t("common.bookmark_types.media"),
})}
</TableCell>
</TableRow>
);
default: {
const _exhaustiveCheck: never = matcher;
return null;
Expand Down
10 changes: 9 additions & 1 deletion apps/web/lib/i18n/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,13 @@
"screenshot": "Screenshot",
"video": "Video",
"archive": "Archive",
"home": "Home"
"home": "Home",
"bookmark_types": {
"title": "Bookmark Type",
"link": "Link",
"text": "Text",
"media": "Media"
}
},
"layouts": {
"masonry": "Masonry",
Expand Down Expand Up @@ -218,6 +224,8 @@
"has_tag": "Has Tag",
"does_not_have_tag": "Does Not Have Tag",
"full_text_search": "Full Text Search",
"type_is": "Type is",
"type_is_not": "Type is not",
"and": "And",
"or": "Or"
},
Expand Down
14 changes: 14 additions & 0 deletions apps/web/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,17 @@ export function getOS() {
}
return os;
}

export function match<T extends string | number | symbol, U>(
val: T,
options: Record<T, U>,
) {
return options[val];
}

export function matchFunc<T extends string | number | symbol, U>(
val: T,
options: Record<T, () => U>,
) {
return options[val]();
}
27 changes: 14 additions & 13 deletions docs/docs/14-Guides/02-search-query-language.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,20 @@ Hoarder provides a search query language to filter and find bookmarks. Here are

Here's a comprehensive table of all supported qualifiers:

| Qualifier | Description | Example Usage |
| --------------- | -------------------------------------------------- | --------------------- |
| `is:fav` | Favorited bookmarks | `is:fav` |
| `is:archived` | Archived bookmarks | `-is:archived` |
| `is:tagged` | Bookmarks that has one or more tags | `is:tagged` |
| `is:inlist` | Bookmarks that are in one or more lists | `is:inlist` |
| `url:<value>` | Match bookmarks with URL substring | `url:example.com` |
| `#<tag>` | Match bookmarks with specific tag | `#important` |
| | Supports quoted strings for tags with spaces | `#"work in progress"` |
| `list:<name>` | Match bookmarks in specific list | `list:reading` |
| | Supports quoted strings for list names with spaces | `list:"to review"` |
| `after:<date>` | Bookmarks created on or after date (YYYY-MM-DD) | `after:2023-01-01` |
| `before:<date>` | Bookmarks created on orbefore date (YYYY-MM-DD) | `before:2023-12-31` |
| Qualifier | Description | Example Usage |
| -------------------------------- | -------------------------------------------------- | --------------------- |
| `is:fav` | Favorited bookmarks | `is:fav` |
| `is:archived` | Archived bookmarks | `-is:archived` |
| `is:tagged` | Bookmarks that has one or more tags | `is:tagged` |
| `is:inlist` | Bookmarks that are in one or more lists | `is:inlist` |
| `is:link`, `is:text`, `is:media` | Bookmarks that are of type link, text or media | `is:link` |
| `url:<value>` | Match bookmarks with URL substring | `url:example.com` |
| `#<tag>` | Match bookmarks with specific tag | `#important` |
| | Supports quoted strings for tags with spaces | `#"work in progress"` |
| `list:<name>` | Match bookmarks in specific list | `list:reading` |
| | Supports quoted strings for list names with spaces | `list:"to review"` |
| `after:<date>` | Bookmarks created on or after date (YYYY-MM-DD) | `after:2023-01-01` |
| `before:<date>` | Bookmarks created on orbefore date (YYYY-MM-DD) | `before:2023-12-31` |

### Examples

Expand Down
55 changes: 55 additions & 0 deletions packages/shared/searchQueryParser.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, expect, test } from "vitest";

import { parseSearchQuery } from "./searchQueryParser";
import { BookmarkTypes } from "./types/bookmarks";

describe("Search Query Parser", () => {
test("simple is queries", () => {
Expand Down Expand Up @@ -68,6 +69,60 @@ describe("Search Query Parser", () => {
inList: false,
},
});
expect(parseSearchQuery("is:link")).toEqual({
result: "full",
text: "",
matcher: {
type: "type",
typeName: BookmarkTypes.LINK,
inverse: false,
},
});
expect(parseSearchQuery("-is:link")).toEqual({
result: "full",
text: "",
matcher: {
type: "type",
typeName: BookmarkTypes.LINK,
inverse: true,
},
});
expect(parseSearchQuery("is:text")).toEqual({
result: "full",
text: "",
matcher: {
type: "type",
typeName: BookmarkTypes.TEXT,
inverse: false,
},
});
expect(parseSearchQuery("-is:text")).toEqual({
result: "full",
text: "",
matcher: {
type: "type",
typeName: BookmarkTypes.TEXT,
inverse: true,
},
});
expect(parseSearchQuery("is:media")).toEqual({
result: "full",
text: "",
matcher: {
type: "type",
typeName: BookmarkTypes.ASSET,
inverse: false,
},
});
expect(parseSearchQuery("-is:media")).toEqual({
result: "full",
text: "",
matcher: {
type: "type",
typeName: BookmarkTypes.ASSET,
inverse: true,
},
});
});

test("simple string queries", () => {
Expand Down
28 changes: 28 additions & 0 deletions packages/shared/searchQueryParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
} from "typescript-parsec";
import { z } from "zod";

import { BookmarkTypes } from "./types/bookmarks";
import { Matcher } from "./types/search";

enum TokenType {
Expand Down Expand Up @@ -136,6 +137,33 @@ MATCHER.setPattern(
text: "",
matcher: { type: "inlist", inList: !minus },
};
case "link":
return {
text: "",
matcher: {
type: "type",
typeName: BookmarkTypes.LINK,
inverse: !!minus,
},
};
case "text":
return {
text: "",
matcher: {
type: "type",
typeName: BookmarkTypes.TEXT,
inverse: !!minus,
},
};
case "media":
return {
text: "",
matcher: {
type: "type",
typeName: BookmarkTypes.ASSET,
inverse: !!minus,
},
};
default:
// If the token is not known, emit it as pure text
return {
Expand Down
14 changes: 14 additions & 0 deletions packages/shared/types/search.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { z } from "zod";

import { BookmarkTypes } from "./bookmarks";

const zTagNameMatcher = z.object({
type: z.literal("tagName"),
tagName: z.string(),
Expand Down Expand Up @@ -50,6 +52,16 @@ const zIsInListMatcher = z.object({
inList: z.boolean(),
});

const zTypeMatcher = z.object({
type: z.literal("type"),
typeName: z.enum([
BookmarkTypes.LINK,
BookmarkTypes.TEXT,
BookmarkTypes.ASSET,
]),
inverse: z.boolean(),
});

const zNonRecursiveMatcher = z.union([
zTagNameMatcher,
zListNameMatcher,
Expand All @@ -60,6 +72,7 @@ const zNonRecursiveMatcher = z.union([
zDateBeforeMatcher,
zIsTaggedMatcher,
zIsInListMatcher,
zTypeMatcher,
]);

type NonRecursiveMatcher = z.infer<typeof zNonRecursiveMatcher>;
Expand All @@ -79,6 +92,7 @@ export const zMatcherSchema: z.ZodType<Matcher> = z.lazy(() => {
zDateBeforeMatcher,
zIsTaggedMatcher,
zIsInListMatcher,
zTypeMatcher,
z.object({
type: z.literal("and"),
matchers: z.array(zMatcherSchema),
Expand Down
48 changes: 48 additions & 0 deletions packages/trpc/lib/__tests__/search.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,54 @@ describe("getBookmarkIdsFromMatcher", () => {
expect(result).toEqual(["b1", "b2"]);
});

it("should handle type matcher", async () => {
expect(
await getBookmarkIdsFromMatcher(mockCtx, {
type: "type",
typeName: BookmarkTypes.LINK,
inverse: false,
}),
).toEqual(["b1", "b2", "b4"]);
expect(
await getBookmarkIdsFromMatcher(mockCtx, {
type: "type",
typeName: BookmarkTypes.TEXT,
inverse: false,
}),
).toEqual(["b3", "b5"]);
expect(
await getBookmarkIdsFromMatcher(mockCtx, {
type: "type",
typeName: BookmarkTypes.ASSET,
inverse: false,
}),
).toEqual(["b6"]);
});

it("should handle type matcher with inverse=true", async () => {
expect(
await getBookmarkIdsFromMatcher(mockCtx, {
type: "type",
typeName: BookmarkTypes.LINK,
inverse: true,
}),
).toEqual(["b3", "b5", "b6"]);
expect(
await getBookmarkIdsFromMatcher(mockCtx, {
type: "type",
typeName: BookmarkTypes.TEXT,
inverse: true,
}),
).toEqual(["b1", "b2", "b4", "b6"]);
expect(
await getBookmarkIdsFromMatcher(mockCtx, {
type: "type",
typeName: BookmarkTypes.ASSET,
inverse: true,
}),
).toEqual(["b1", "b2", "b3", "b4", "b5"]);
});

it("should handle dateBefore matcher with inverse=true", async () => {
const matcher: Matcher = {
type: "dateBefore",
Expand Down
10 changes: 10 additions & 0 deletions packages/trpc/lib/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
like,
lt,
lte,
ne,
notExists,
notLike,
} from "drizzle-orm";
Expand Down Expand Up @@ -233,6 +234,15 @@ async function getIds(
),
);
}
case "type": {
const comp = matcher.inverse
? ne(bookmarks.type, matcher.typeName)
: eq(bookmarks.type, matcher.typeName);
return db
.select({ id: bookmarks.id })
.from(bookmarks)
.where(and(eq(bookmarks.userId, userId), comp));
}
case "and": {
const vals = await Promise.all(
matcher.matchers.map((m) => getIds(db, userId, m)),
Expand Down

0 comments on commit 9fd26b4

Please sign in to comment.