aboutsummaryrefslogtreecommitdiffstats
path: root/apps/web/app
diff options
context:
space:
mode:
authorMohamed Bassem <me@mbassem.com>2025-04-27 00:02:20 +0100
committerGitHub <noreply@github.com>2025-04-27 00:02:20 +0100
commit136f126296af65f50da598d084d1485c0e40437a (patch)
tree2725c7932ebbcb9b48b5af98eb9b72329a400260 /apps/web/app
parentca47be7fe7be128f459c37614a04902a873fe289 (diff)
downloadkarakeep-136f126296af65f50da598d084d1485c0e40437a.tar.zst
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
Diffstat (limited to 'apps/web/app')
-rw-r--r--apps/web/app/layout.tsx6
-rw-r--r--apps/web/app/settings/layout.tsx6
-rw-r--r--apps/web/app/settings/rules/page.tsx89
3 files changed, 100 insertions, 1 deletions
diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx
index 6b75edf3..beeecc2b 100644
--- a/apps/web/app/layout.tsx
+++ b/apps/web/app/layout.tsx
@@ -49,7 +49,11 @@ export default async function RootLayout({
const userSettings = await getUserLocalSettings();
const isRTL = userSettings.lang === "ar";
return (
- <html lang={userSettings.lang} dir={isRTL ? "rtl" : "ltr"}>
+ <html
+ className="sm:overflow-hidden"
+ lang={userSettings.lang}
+ dir={isRTL ? "rtl" : "ltr"}
+ >
<body className={inter.className}>
<Providers
session={session}
diff --git a/apps/web/app/settings/layout.tsx b/apps/web/app/settings/layout.tsx
index 62ac041c..9bac783c 100644
--- a/apps/web/app/settings/layout.tsx
+++ b/apps/web/app/settings/layout.tsx
@@ -5,6 +5,7 @@ import { TFunction } from "i18next";
import {
ArrowLeft,
Download,
+ GitBranch,
Image,
KeyRound,
Link,
@@ -62,6 +63,11 @@ const settingsSidebarItems = (
path: "/settings/webhooks",
},
{
+ name: t("settings.rules.rules"),
+ icon: <GitBranch size={18} />,
+ path: "/settings/rules",
+ },
+ {
name: t("settings.manage_assets.manage_assets"),
icon: <Image size={18} />,
path: "/settings/assets",
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>
+ );
+}