diff options
| -rw-r--r-- | apps/web/components/dashboard/rules/RuleEngineConditionBuilder.tsx | 19 | ||||
| -rw-r--r-- | apps/web/lib/i18n/locales/en/translation.json | 1 | ||||
| -rw-r--r-- | packages/shared/types/rules.ts | 8 | ||||
| -rw-r--r-- | packages/trpc/lib/__tests__/ruleEngine.test.ts | 16 | ||||
| -rw-r--r-- | packages/trpc/lib/ruleEngine.ts | 7 |
5 files changed, 51 insertions, 0 deletions
diff --git a/apps/web/components/dashboard/rules/RuleEngineConditionBuilder.tsx b/apps/web/components/dashboard/rules/RuleEngineConditionBuilder.tsx index 8faca013..a859a4cc 100644 --- a/apps/web/components/dashboard/rules/RuleEngineConditionBuilder.tsx +++ b/apps/web/components/dashboard/rules/RuleEngineConditionBuilder.tsx @@ -54,6 +54,9 @@ export function ConditionBuilder({ case "urlContains": onChange({ type: "urlContains", str: "" }); break; + case "urlDoesNotContain": + onChange({ type: "urlDoesNotContain", str: "" }); + break; case "importedFromFeed": onChange({ type: "importedFromFeed", feedId: "" }); break; @@ -88,6 +91,7 @@ export function ConditionBuilder({ const renderConditionIcon = (type: RuleEngineCondition["type"]) => { switch (type) { case "urlContains": + case "urlDoesNotContain": return <Link className="h-4 w-4" />; case "importedFromFeed": return <Rss className="h-4 w-4" />; @@ -118,6 +122,18 @@ export function ConditionBuilder({ </div> ); + case "urlDoesNotContain": + return ( + <div className="mt-2"> + <Input + value={value.str} + onChange={(e) => onChange({ ...value, str: e.target.value })} + placeholder="URL does not contain..." + className="w-full" + /> + </div> + ); + case "importedFromFeed": return ( <div className="mt-2"> @@ -235,6 +251,9 @@ export function ConditionBuilder({ <SelectItem value="urlContains"> {t("settings.rules.conditions_types.url_contains")} </SelectItem> + <SelectItem value="urlDoesNotContain"> + {t("settings.rules.conditions_types.url_does_not_contain")} + </SelectItem> <SelectItem value="importedFromFeed"> {t("settings.rules.conditions_types.imported_from_feed")} </SelectItem> diff --git a/apps/web/lib/i18n/locales/en/translation.json b/apps/web/lib/i18n/locales/en/translation.json index 58e1af09..87851324 100644 --- a/apps/web/lib/i18n/locales/en/translation.json +++ b/apps/web/lib/i18n/locales/en/translation.json @@ -348,6 +348,7 @@ "conditions_types": { "always": "Always", "url_contains": "URL Contains", + "url_does_not_contain": "URL Does Not Contain", "imported_from_feed": "Imported From Feed", "bookmark_type_is": "Bookmark Type Is", "has_tag": "Has Tag", diff --git a/packages/shared/types/rules.ts b/packages/shared/types/rules.ts index 92300b3c..0daec524 100644 --- a/packages/shared/types/rules.ts +++ b/packages/shared/types/rules.ts @@ -54,6 +54,11 @@ const zUrlContainsCondition = z.object({ str: z.string(), }); +const zUrlDoesNotContainCondition = z.object({ + type: z.literal("urlDoesNotContain"), + str: z.string(), +}); + const zImportedFromFeedCondition = z.object({ type: z.literal("importedFromFeed"), feedId: z.string(), @@ -80,6 +85,7 @@ const zIsArchivedCondition = z.object({ const nonRecursiveCondition = z.discriminatedUnion("type", [ zAlwaysTrueCondition, zUrlContainsCondition, + zUrlDoesNotContainCondition, zImportedFromFeedCondition, zBookmarkTypeIsCondition, zHasTagCondition, @@ -98,6 +104,7 @@ export const zRuleEngineConditionSchema: z.ZodType<RuleEngineCondition> = z.discriminatedUnion("type", [ zAlwaysTrueCondition, zUrlContainsCondition, + zUrlDoesNotContainCondition, zImportedFromFeedCondition, zBookmarkTypeIsCondition, zHasTagCondition, @@ -227,6 +234,7 @@ const ruleValidaitorFn = ( case "isArchived": return true; case "urlContains": + case "urlDoesNotContain": if (condition.str.length == 0) { ctx.addIssue({ code: "custom", diff --git a/packages/trpc/lib/__tests__/ruleEngine.test.ts b/packages/trpc/lib/__tests__/ruleEngine.test.ts index ede22ec6..600d8aa9 100644 --- a/packages/trpc/lib/__tests__/ruleEngine.test.ts +++ b/packages/trpc/lib/__tests__/ruleEngine.test.ts @@ -219,6 +219,22 @@ describe("RuleEngine", () => { expect(engine.doesBookmarkMatchConditions(condition)).toBe(false); }); + it("should return false for urlDoesNotContain condition when URL contains string", () => { + const condition: RuleEngineCondition = { + type: "urlDoesNotContain", + str: "example.com", + }; + expect(engine.doesBookmarkMatchConditions(condition)).toBe(false); + }); + + it("should return true for urlDoesNotContain condition when URL does not contain string", () => { + const condition: RuleEngineCondition = { + type: "urlDoesNotContain", + str: "nonexistent", + }; + expect(engine.doesBookmarkMatchConditions(condition)).toBe(true); + }); + it("should return true for importedFromFeed condition", () => { const condition: RuleEngineCondition = { type: "importedFromFeed", diff --git a/packages/trpc/lib/ruleEngine.ts b/packages/trpc/lib/ruleEngine.ts index c191619b..6b5f8fdf 100644 --- a/packages/trpc/lib/ruleEngine.ts +++ b/packages/trpc/lib/ruleEngine.ts @@ -3,6 +3,7 @@ import { and, eq } from "drizzle-orm"; import { bookmarks, tagsOnBookmarks } from "@karakeep/db/schema"; import { LinkCrawlerQueue } from "@karakeep/shared-server"; +import { BookmarkTypes } from "@karakeep/shared/types/bookmarks"; import { RuleEngineAction, RuleEngineCondition, @@ -83,6 +84,12 @@ export class RuleEngine { case "urlContains": { return (this.bookmark.link?.url ?? "").includes(condition.str); } + case "urlDoesNotContain": { + return ( + this.bookmark.type == BookmarkTypes.LINK && + !(this.bookmark.link?.url ?? "").includes(condition.str) + ); + } case "importedFromFeed": { return this.bookmark.rssFeeds.some( (f) => f.rssFeedId === condition.feedId, |
