From 136f126296af65f50da598d084d1485c0e40437a Mon Sep 17 00:00:00 2001 From: Mohamed Bassem Date: Sun, 27 Apr 2025 00:02:20 +0100 Subject: feat: Implement generic rule engine (#1318) * Add schema for the new rule engine * Add rule engine backend logic * Implement the worker logic and event firing * Implement the UI changesfor the rule engine * Ensure that when a referenced list or tag are deleted, the corresponding event/action is * Dont show smart lists in rule engine events * Add privacy validations for attached tag and list ids * Move the rules logic into a models --- .../dashboard/rules/RuleEngineConditionBuilder.tsx | 322 +++++++++++++++++++++ 1 file changed, 322 insertions(+) create mode 100644 apps/web/components/dashboard/rules/RuleEngineConditionBuilder.tsx (limited to 'apps/web/components/dashboard/rules/RuleEngineConditionBuilder.tsx') diff --git a/apps/web/components/dashboard/rules/RuleEngineConditionBuilder.tsx b/apps/web/components/dashboard/rules/RuleEngineConditionBuilder.tsx new file mode 100644 index 00000000..8faca013 --- /dev/null +++ b/apps/web/components/dashboard/rules/RuleEngineConditionBuilder.tsx @@ -0,0 +1,322 @@ +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Archive, + ChevronDown, + ChevronRight, + FileType, + Link, + PlusCircle, + Rss, + Star, + Tag, + Trash2, +} from "lucide-react"; +import { useTranslation } from "react-i18next"; + +import type { RuleEngineCondition } from "@karakeep/shared/types/rules"; + +import { FeedSelector } from "../feeds/FeedSelector"; +import { TagAutocomplete } from "../tags/TagAutocomplete"; + +interface ConditionBuilderProps { + value: RuleEngineCondition; + onChange: (condition: RuleEngineCondition) => void; + level?: number; + onRemove?: () => void; +} + +export function ConditionBuilder({ + value, + onChange, + level = 0, + onRemove, +}: ConditionBuilderProps) { + const { t } = useTranslation(); + const [isOpen, setIsOpen] = useState(true); + + const handleTypeChange = (type: RuleEngineCondition["type"]) => { + switch (type) { + case "urlContains": + onChange({ type: "urlContains", str: "" }); + break; + case "importedFromFeed": + onChange({ type: "importedFromFeed", feedId: "" }); + break; + case "bookmarkTypeIs": + onChange({ type: "bookmarkTypeIs", bookmarkType: "link" }); + break; + case "hasTag": + onChange({ type: "hasTag", tagId: "" }); + break; + case "isFavourited": + onChange({ type: "isFavourited" }); + break; + case "isArchived": + onChange({ type: "isArchived" }); + break; + case "and": + onChange({ type: "and", conditions: [] }); + break; + case "or": + onChange({ type: "or", conditions: [] }); + break; + case "alwaysTrue": + onChange({ type: "alwaysTrue" }); + break; + default: { + const _exhaustiveCheck: never = type; + return null; + } + } + }; + + const renderConditionIcon = (type: RuleEngineCondition["type"]) => { + switch (type) { + case "urlContains": + return ; + case "importedFromFeed": + return ; + case "bookmarkTypeIs": + return ; + case "hasTag": + return ; + case "isFavourited": + return ; + case "isArchived": + return ; + default: + return null; + } + }; + + const renderConditionFields = () => { + switch (value.type) { + case "urlContains": + return ( +
+ onChange({ ...value, str: e.target.value })} + placeholder="URL contains..." + className="w-full" + /> +
+ ); + + case "importedFromFeed": + return ( +
+ onChange({ ...value, feedId: e })} + className="w-full" + /> +
+ ); + + case "bookmarkTypeIs": + return ( +
+ +
+ ); + + case "hasTag": + return ( +
+ onChange({ type: value.type, tagId: t })} + /> +
+ ); + + case "and": + case "or": + return ( +
+ {value.conditions.map((condition, index) => ( + { + const newConditions = [...value.conditions]; + newConditions[index] = newCondition; + onChange({ ...value, conditions: newConditions }); + }} + level={level + 1} + onRemove={() => { + const newConditions = [...value.conditions]; + newConditions.splice(index, 1); + onChange({ ...value, conditions: newConditions }); + }} + /> + ))} + + +
+ ); + + default: + return null; + } + }; + + const ConditionSelector = () => ( + + ); + + return ( + + + {value.type === "and" || value.type === "or" ? ( + +
+
+ + + + + + {value.conditions.length} condition + {value.conditions.length !== 1 ? "s" : ""} + +
+ + {onRemove && ( + + )} +
+ + {renderConditionFields()} +
+ ) : ( +
+
+
+ {renderConditionIcon(value.type)} + +
+ + {onRemove && ( + + )} +
+ + {renderConditionFields()} +
+ )} +
+
+ ); +} -- cgit v1.2.3-70-g09d2