aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--apps/web/components/dashboard/rules/RuleEngineConditionBuilder.tsx19
-rw-r--r--apps/web/lib/i18n/locales/en/translation.json1
-rw-r--r--packages/shared/types/rules.ts8
-rw-r--r--packages/trpc/lib/__tests__/ruleEngine.test.ts16
-rw-r--r--packages/trpc/lib/ruleEngine.ts7
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,