diff options
| author | Mohamed Bassem <me@mbassem.com> | 2025-04-27 00:02:20 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-04-27 00:02:20 +0100 |
| commit | 136f126296af65f50da598d084d1485c0e40437a (patch) | |
| tree | 2725c7932ebbcb9b48b5af98eb9b72329a400260 /apps/web/components/dashboard/rules/RuleEngineActionBuilder.tsx | |
| parent | ca47be7fe7be128f459c37614a04902a873fe289 (diff) | |
| download | karakeep-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/components/dashboard/rules/RuleEngineActionBuilder.tsx')
| -rw-r--r-- | apps/web/components/dashboard/rules/RuleEngineActionBuilder.tsx | 216 |
1 files changed, 216 insertions, 0 deletions
diff --git a/apps/web/components/dashboard/rules/RuleEngineActionBuilder.tsx b/apps/web/components/dashboard/rules/RuleEngineActionBuilder.tsx new file mode 100644 index 00000000..0354e8ac --- /dev/null +++ b/apps/web/components/dashboard/rules/RuleEngineActionBuilder.tsx @@ -0,0 +1,216 @@ +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Archive, + Download, + List, + PlusCircle, + Star, + Tag, + Trash2, +} from "lucide-react"; +import { useTranslation } from "react-i18next"; + +import type { RuleEngineAction } from "@karakeep/shared/types/rules"; + +import { BookmarkListSelector } from "../lists/BookmarkListSelector"; +import { TagAutocomplete } from "../tags/TagAutocomplete"; + +interface ActionBuilderProps { + value: RuleEngineAction[]; + onChange: (actions: RuleEngineAction[]) => void; +} + +export function ActionBuilder({ value, onChange }: ActionBuilderProps) { + const { t } = useTranslation(); + const handleAddAction = () => { + onChange([...value, { type: "addTag", tagId: "" }]); + }; + + const handleRemoveAction = (index: number) => { + const newActions = [...value]; + newActions.splice(index, 1); + onChange(newActions); + }; + + const handleActionTypeChange = ( + index: number, + type: RuleEngineAction["type"], + ) => { + const newActions = [...value]; + + switch (type) { + case "addTag": + newActions[index] = { type: "addTag", tagId: "" }; + break; + case "removeTag": + newActions[index] = { type: "removeTag", tagId: "" }; + break; + case "addToList": + newActions[index] = { type: "addToList", listId: "" }; + break; + case "removeFromList": + newActions[index] = { type: "removeFromList", listId: "" }; + break; + case "downloadFullPageArchive": + newActions[index] = { type: "downloadFullPageArchive" }; + break; + case "favouriteBookmark": + newActions[index] = { type: "favouriteBookmark" }; + break; + case "archiveBookmark": + newActions[index] = { type: "archiveBookmark" }; + break; + default: { + const _exhaustiveCheck: never = type; + return null; + } + } + + onChange(newActions); + }; + + const handleActionFieldChange = ( + index: number, + selectVal: RuleEngineAction, + ) => { + const newActions = [...value]; + newActions[index] = selectVal; + onChange(newActions); + }; + + const renderActionIcon = (type: string) => { + switch (type) { + case "addTag": + case "removeTag": + return <Tag className="h-4 w-4" />; + case "addToList": + case "removeFromList": + return <List className="h-4 w-4" />; + case "downloadFullPageArchive": + return <Download className="h-4 w-4" />; + case "favouriteBookmark": + return <Star className="h-4 w-4" />; + case "archiveBookmark": + return <Archive className="h-4 w-4" />; + default: + return null; + } + }; + + return ( + <div className="space-y-3"> + {value.length === 0 ? ( + <div className="rounded-md border border-dashed p-4 text-center"> + <p className="text-sm text-muted-foreground">No actions added yet</p> + </div> + ) : ( + value.map((action, index) => ( + <Card key={index}> + <CardContent className="p-3"> + <div className="flex items-center justify-between"> + <div className="flex flex-1 items-center"> + {renderActionIcon(action.type)} + <Select + value={action.type} + onValueChange={(value) => + handleActionTypeChange( + index, + value as RuleEngineAction["type"], + ) + } + > + <SelectTrigger className="ml-2 h-8 w-auto border-none bg-transparent px-2"> + <SelectValue /> + </SelectTrigger> + <SelectContent> + <SelectItem value="addTag"> + {t("settings.rules.actions_types.add_tag")} + </SelectItem> + <SelectItem value="removeTag"> + {t("settings.rules.actions_types.remove_tag")} + </SelectItem> + <SelectItem value="addToList"> + {t("settings.rules.actions_types.add_to_list")} + </SelectItem> + <SelectItem value="removeFromList"> + {t("settings.rules.actions_types.remove_from_list")} + </SelectItem> + <SelectItem value="downloadFullPageArchive"> + {t( + "settings.rules.actions_types.download_full_page_archive", + )} + </SelectItem> + <SelectItem value="favouriteBookmark"> + {t("settings.rules.actions_types.favourite_bookmark")} + </SelectItem> + <SelectItem value="archiveBookmark"> + {t("settings.rules.actions_types.archive_bookmark")} + </SelectItem> + </SelectContent> + </Select> + + {(action.type === "addTag" || + action.type === "removeTag") && ( + <TagAutocomplete + className="ml-2 h-8 flex-1" + tagId={action.tagId} + onChange={(t) => + handleActionFieldChange(index, { + type: action.type, + tagId: t, + }) + } + /> + )} + + {(action.type === "addToList" || + action.type === "removeFromList") && ( + <BookmarkListSelector + className="ml-2 h-8 flex-1" + value={action.listId} + listTypes={["manual"]} + onChange={(e) => + handleActionFieldChange(index, { + type: action.type, + listId: e, + }) + } + /> + )} + </div> + + <Button + variant="ghost" + size="sm" + onClick={() => handleRemoveAction(index)} + className="h-7 w-7 p-0" + > + <Trash2 className="h-4 w-4 text-red-500" /> + </Button> + </div> + </CardContent> + </Card> + )) + )} + + <Button + type="button" + variant="outline" + size="sm" + onClick={handleAddAction} + className="w-full" + > + <PlusCircle className="mr-2 h-4 w-4" /> + Add Action + </Button> + </div> + ); +} |
