From c56cf4e24f6134547fb9c5b58eb20840f5083e9e Mon Sep 17 00:00:00 2001 From: Andrii Mokhovyk Date: Sun, 18 Jan 2026 16:36:49 +0200 Subject: feat(rules): add "Title Contains" condition to Rule Engine (#1670) (#2354) * feat(rules): add "Title Contains" condition to Rule Engine (#1670) * feat(rules): hide title conditions for bookmark created trigger * fix typecheck --- .../dashboard/rules/RuleEngineConditionBuilder.tsx | 56 +++++++++++++++++++++- .../dashboard/rules/RuleEngineRuleEditor.tsx | 1 + 2 files changed, 56 insertions(+), 1 deletion(-) (limited to 'apps/web/components/dashboard/rules') diff --git a/apps/web/components/dashboard/rules/RuleEngineConditionBuilder.tsx b/apps/web/components/dashboard/rules/RuleEngineConditionBuilder.tsx index a859a4cc..28bf690d 100644 --- a/apps/web/components/dashboard/rules/RuleEngineConditionBuilder.tsx +++ b/apps/web/components/dashboard/rules/RuleEngineConditionBuilder.tsx @@ -19,6 +19,7 @@ import { ChevronDown, ChevronRight, FileType, + Heading, Link, PlusCircle, Rss, @@ -28,7 +29,10 @@ import { } from "lucide-react"; import { useTranslation } from "react-i18next"; -import type { RuleEngineCondition } from "@karakeep/shared/types/rules"; +import type { + RuleEngineCondition, + RuleEngineEvent, +} from "@karakeep/shared/types/rules"; import { FeedSelector } from "../feeds/FeedSelector"; import { TagAutocomplete } from "../tags/TagAutocomplete"; @@ -36,6 +40,7 @@ import { TagAutocomplete } from "../tags/TagAutocomplete"; interface ConditionBuilderProps { value: RuleEngineCondition; onChange: (condition: RuleEngineCondition) => void; + eventType: RuleEngineEvent["type"]; level?: number; onRemove?: () => void; } @@ -43,6 +48,7 @@ interface ConditionBuilderProps { export function ConditionBuilder({ value, onChange, + eventType, level = 0, onRemove, }: ConditionBuilderProps) { @@ -57,6 +63,12 @@ export function ConditionBuilder({ case "urlDoesNotContain": onChange({ type: "urlDoesNotContain", str: "" }); break; + case "titleContains": + onChange({ type: "titleContains", str: "" }); + break; + case "titleDoesNotContain": + onChange({ type: "titleDoesNotContain", str: "" }); + break; case "importedFromFeed": onChange({ type: "importedFromFeed", feedId: "" }); break; @@ -93,6 +105,9 @@ export function ConditionBuilder({ case "urlContains": case "urlDoesNotContain": return ; + case "titleContains": + case "titleDoesNotContain": + return ; case "importedFromFeed": return ; case "bookmarkTypeIs": @@ -134,6 +149,30 @@ export function ConditionBuilder({ ); + case "titleContains": + return ( +
+ onChange({ ...value, str: e.target.value })} + placeholder="Title contains..." + className="w-full" + /> +
+ ); + + case "titleDoesNotContain": + return ( +
+ onChange({ ...value, str: e.target.value })} + placeholder="Title does not contain..." + className="w-full" + /> +
+ ); + case "importedFromFeed": return (
@@ -198,6 +237,7 @@ export function ConditionBuilder({ newConditions[index] = newCondition; onChange({ ...value, conditions: newConditions }); }} + eventType={eventType} level={level + 1} onRemove={() => { const newConditions = [...value.conditions]; @@ -233,6 +273,10 @@ export function ConditionBuilder({ } }; + // Title conditions are hidden for "bookmarkAdded" event because + // titles are not available at bookmark creation time (they're fetched during crawling) + const showTitleConditions = eventType !== "bookmarkAdded"; + const ConditionSelector = () => (