From 136f126296af65f50da598d084d1485c0e40437a Mon Sep 17 00:00:00 2001
From: Mohamed Bassem <me@mbassem.com>
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
---
 apps/web/app/settings/rules/page.tsx | 89 ++++++++++++++++++++++++++++++++++++
 1 file changed, 89 insertions(+)
 create mode 100644 apps/web/app/settings/rules/page.tsx

(limited to 'apps/web/app/settings/rules/page.tsx')

diff --git a/apps/web/app/settings/rules/page.tsx b/apps/web/app/settings/rules/page.tsx
new file mode 100644
index 00000000..98a30bcc
--- /dev/null
+++ b/apps/web/app/settings/rules/page.tsx
@@ -0,0 +1,89 @@
+"use client";
+
+import { useState } from "react";
+import { RuleEditor } from "@/components/dashboard/rules/RuleEngineRuleEditor";
+import RuleList from "@/components/dashboard/rules/RuleEngineRuleList";
+import { Button } from "@/components/ui/button";
+import { FullPageSpinner } from "@/components/ui/full-page-spinner";
+import { useTranslation } from "@/lib/i18n/client";
+import { api } from "@/lib/trpc";
+import { Tooltip, TooltipContent, TooltipTrigger } from "components/ui/tooltip";
+import { FlaskConical, PlusCircle } from "lucide-react";
+
+import { RuleEngineRule } from "@karakeep/shared/types/rules";
+
+export default function RulesSettingsPage() {
+  const { t } = useTranslation();
+  const [editingRule, setEditingRule] = useState<
+    (Omit<RuleEngineRule, "id"> & { id: string | null }) | null
+  >(null);
+
+  const { data: rules, isLoading } = api.rules.list.useQuery(undefined, {
+    refetchOnWindowFocus: true,
+    refetchOnMount: true,
+  });
+
+  const handleCreateRule = () => {
+    const newRule = {
+      id: null,
+      name: "New Rule",
+      description: "Description of the new rule",
+      enabled: true,
+      event: { type: "bookmarkAdded" as const },
+      condition: { type: "alwaysTrue" as const },
+      actions: [{ type: "addTag" as const, tagId: "" }],
+    };
+    setEditingRule(newRule);
+  };
+
+  const handleDeleteRule = (ruleId: string) => {
+    if (editingRule?.id === ruleId) {
+      // If the rule being edited is being deleted, reset the editing rule
+      setEditingRule(null);
+    }
+  };
+
+  return (
+    <div className="rounded-md border bg-background p-4">
+      <div className="flex flex-col gap-2">
+        <div className="flex items-center justify-between">
+          <span className="flex items-center gap-2 text-lg font-medium">
+            {t("settings.rules.rules")}
+            <Tooltip>
+              <TooltipTrigger className="text-muted-foreground">
+                <FlaskConical size={15} />
+              </TooltipTrigger>
+              <TooltipContent side="bottom">
+                {t("common.experimental")}
+              </TooltipContent>
+            </Tooltip>
+          </span>
+          <Button onClick={handleCreateRule} variant="default">
+            <PlusCircle className="mr-2 h-4 w-4" />
+            {t("settings.rules.ceate_rule")}
+          </Button>
+        </div>
+        <p className="text-sm italic text-muted-foreground">
+          {t("settings.rules.description")}
+        </p>
+        {!rules || isLoading ? (
+          <FullPageSpinner />
+        ) : (
+          <RuleList
+            rules={rules.rules}
+            onEditRule={(r) => setEditingRule(r)}
+            onDeleteRule={handleDeleteRule}
+          />
+        )}
+        <div className="lg:col-span-7">
+          {editingRule && (
+            <RuleEditor
+              rule={editingRule}
+              onCancel={() => setEditingRule(null)}
+            />
+          )}
+        </div>
+      </div>
+    </div>
+  );
+}
-- 
cgit v1.2.3-70-g09d2

