From afefbb8d46c3e3c9c5e25cc042ba94f5ecb10668 Mon Sep 17 00:00:00 2001
From: Linus Rath <139418639+rathlinus@users.noreply.github.com>
Date: Sat, 21 Mar 2026 14:16:54 +0100
Subject: [PATCH] feat: add expanded visual view for filter rules
- Add VisualRuleSummary component showing conditions and actions as
labeled inline pills with IF/THEN flow layout
- Add expandedFilterView toggle to settings store (persisted)
- Fix RuleSummary to allow multi-line wrapping instead of truncating
- Use items-start on rule cards so drag handle and toggle align to top
- Add translations for expanded view keys in all 8 locales
---
components/settings/filter-settings.tsx | 149 +++++++++++++++++++-----
locales/de/common.json | 6 +
locales/en/common.json | 6 +
locales/es/common.json | 6 +
locales/fr/common.json | 6 +
locales/it/common.json | 6 +
locales/ja/common.json | 6 +
locales/nl/common.json | 6 +
locales/pt/common.json | 6 +
stores/settings-store.ts | 7 ++
10 files changed, 173 insertions(+), 31 deletions(-)
diff --git a/components/settings/filter-settings.tsx b/components/settings/filter-settings.tsx
index 1799f26e..a7101d55 100644
--- a/components/settings/filter-settings.tsx
+++ b/components/settings/filter-settings.tsx
@@ -9,6 +9,7 @@ import { SieveEditorModal } from "@/components/filters/sieve-editor-modal";
import { useFilterStore } from "@/stores/filter-store";
import { useAuthStore } from "@/stores/auth-store";
import { useEmailStore } from "@/stores/email-store";
+import { useSettingsStore } from "@/stores/settings-store";
import { toast } from "@/stores/toast-store";
import type { FilterRule } from "@/lib/jmap/sieve-types";
import {
@@ -25,31 +26,98 @@ import {
function RuleSummary({ rule }: { rule: FilterRule }) {
const t = useTranslations("settings.filters");
- const conditionSummary = rule.conditions
- .slice(0, 2)
- .map((c) => {
- const field = t(`condition_fields.${c.field}`);
- const comparator = t(`comparators.${c.comparator}`);
- return `${field} ${comparator} "${c.value}"`;
- })
- .join(rule.matchType === "all" ? ` ${t("and")} ` : ` ${t("or")} `);
+ const conditions = rule.conditions.slice(0, 2).map((c) => {
+ const field = t(`condition_fields.${c.field}`);
+ const comparator = t(`comparators.${c.comparator}`);
+ return `${field} ${comparator} "${c.value}"`;
+ });
+
+ const joiner = rule.matchType === "all" ? t("and") : t("or");
const extra = rule.conditions.length > 2
? ` (+${rule.conditions.length - 2})`
: "";
- const actionSummary = rule.actions
- .slice(0, 2)
- .map((a) => {
- const action = t(`action_types.${a.type}`);
- return a.value ? `${action} "${a.value}"` : action;
- })
- .join(", ");
+ const actions = rule.actions.slice(0, 2).map((a) => {
+ const action = t(`action_types.${a.type}`);
+ return a.value ? `${action} "${a.value}"` : action;
+ });
return (
-
- {conditionSummary}{extra} → {actionSummary}
-
+
+
+ {conditions.map((cond, i) => (
+
+ {i > 0 && {joiner} }
+ {cond}
+
+ ))}
+ {extra}
+
+ →
+
+ {actions.map((act, i) => (
+
+ {i > 0 && ", "}
+ {act}
+
+ ))}
+
+
+ );
+}
+
+function VisualRuleSummary({ rule }: { rule: FilterRule }) {
+ const t = useTranslations("settings.filters");
+
+ const joiner = rule.matchType === "all" ? t("and") : t("or");
+ const matchLabel = rule.matchType === "all" ? t("match_all_conditions") : t("match_any_condition");
+
+ return (
+
+
+
+ {t("if")}
+
+ {rule.conditions.map((c, i) => {
+ const field = t(`condition_fields.${c.field}`);
+ const comparator = t(`comparators.${c.comparator}`);
+ return (
+
+ {i > 0 && (
+ {joiner}
+ )}
+
+ {field}
+ {comparator}
+ “{c.value}”
+
+
+ );
+ })}
+ ({matchLabel})
+
+
+
+
+ {t("then")}
+
+ {rule.actions.map((a, i) => {
+ const action = t(`action_types.${a.type}`);
+ return (
+
+ {i > 0 && (
+ ›
+ )}
+
+ {action}
+ {a.value && “{a.value}”}
+
+
+ );
+ })}
+
+
);
}
@@ -58,6 +126,8 @@ export function FilterSettings() {
const tNotifications = useTranslations("notifications");
const { client } = useAuthStore();
const mailboxes = useEmailStore((s) => s.mailboxes);
+ const expandedFilterView = useSettingsStore((s) => s.expandedFilterView);
+ const updateSetting = useSettingsStore((s) => s.updateSetting);
const {
rules,
@@ -337,23 +407,25 @@ export function FilterSettings() {
onDragOver={(e) => handleDragOver(e, index)}
onDrop={(e) => handleDrop(e, index)}
onDragEnd={handleDragEnd}
- className={`flex items-center gap-3 p-3 rounded-md border transition-colors ${
+ className={`flex items-start gap-3 p-3 rounded-md border transition-colors ${
dragOverIndex === index
? "border-primary bg-primary/5"
: "border-border hover:bg-muted/50"
} ${!rule.enabled ? "opacity-60" : ""}`}
>
- handleToggle(rule.id)}
- />
+
+ handleToggle(rule.id)}
+ />
+
{rule.name}
-
+ {expandedFilterView ? (
+
+ ) : (
+
+ )}
{deleteConfirmId === rule.id ? (
@@ -435,12 +511,23 @@ export function FilterSettings() {
- {isSaving && (
-
-
- {t("saving")}
-
- )}
+
+ {isSaving && (
+
+
+ {t("saving")}
+
+ )}
+ {!isOpaque && rules.length > 0 && (
+
+ {t("expanded_view")}
+ updateSetting("expandedFilterView", v)}
+ />
+
+ )}
+
{showRuleModal && (
diff --git a/locales/de/common.json b/locales/de/common.json
index e346d95d..810f9138 100644
--- a/locales/de/common.json
+++ b/locales/de/common.json
@@ -1183,6 +1183,12 @@
"opaque_warning": "Dieses Skript wurde außerhalb des visuellen Builders bearbeitet. Nur die Sieve-Skriptbearbeitung ist verfügbar.",
"open_sieve_editor": "Sieve-Skript-Editor öffnen",
"fetch_error": "Filter konnten nicht geladen werden",
+ "expanded_view": "Erweiterte Ansicht",
+ "expanded_view_description": "Filterregeln mit detaillierten Bedingungs- und Aktionsblöcken anzeigen",
+ "if": "Wenn",
+ "then": "Dann",
+ "match_all_conditions": "alle zutreffen",
+ "match_any_condition": "eine zutrifft",
"and": "und",
"or": "oder",
"cancel": "Abbrechen",
diff --git a/locales/en/common.json b/locales/en/common.json
index 3c1c3a78..a28fe788 100644
--- a/locales/en/common.json
+++ b/locales/en/common.json
@@ -1183,6 +1183,12 @@
"opaque_warning": "This script was edited outside the visual builder. Only raw Sieve editing is available.",
"open_sieve_editor": "Open raw Sieve editor",
"fetch_error": "Failed to load filters",
+ "expanded_view": "Expanded view",
+ "expanded_view_description": "Show filter rules with detailed condition and action blocks",
+ "if": "If",
+ "then": "Then",
+ "match_all_conditions": "all match",
+ "match_any_condition": "any matches",
"and": "and",
"or": "or",
"cancel": "Cancel",
diff --git a/locales/es/common.json b/locales/es/common.json
index 70186d5f..607d0b97 100644
--- a/locales/es/common.json
+++ b/locales/es/common.json
@@ -1183,6 +1183,12 @@
"opaque_warning": "Este script fue editado fuera del constructor visual. Solo está disponible la edición Sieve sin formato.",
"open_sieve_editor": "Abrir editor Sieve",
"fetch_error": "Error al cargar los filtros",
+ "expanded_view": "Vista expandida",
+ "expanded_view_description": "Mostrar reglas de filtro con bloques detallados de condiciones y acciones",
+ "if": "Si",
+ "then": "Entonces",
+ "match_all_conditions": "todas coinciden",
+ "match_any_condition": "alguna coincide",
"and": "y",
"or": "o",
"cancel": "Cancelar",
diff --git a/locales/fr/common.json b/locales/fr/common.json
index 62c22f76..518c219f 100644
--- a/locales/fr/common.json
+++ b/locales/fr/common.json
@@ -1183,6 +1183,12 @@
"opaque_warning": "Ce script a été modifié en dehors du constructeur visuel. Seule l'édition Sieve brute est disponible.",
"open_sieve_editor": "Ouvrir l'éditeur Sieve brut",
"fetch_error": "Échec du chargement des filtres",
+ "expanded_view": "Vue étendue",
+ "expanded_view_description": "Afficher les règles de filtre avec des blocs de conditions et d'actions détaillés",
+ "if": "Si",
+ "then": "Alors",
+ "match_all_conditions": "toutes correspondent",
+ "match_any_condition": "une correspond",
"and": "et",
"or": "ou",
"cancel": "Annuler",
diff --git a/locales/it/common.json b/locales/it/common.json
index 677512b2..3a204e69 100644
--- a/locales/it/common.json
+++ b/locales/it/common.json
@@ -1183,6 +1183,12 @@
"opaque_warning": "Questo script è stato modificato al di fuori del costruttore visuale. È disponibile solo la modifica Sieve grezza.",
"open_sieve_editor": "Apri editor Sieve",
"fetch_error": "Impossibile caricare i filtri",
+ "expanded_view": "Vista espansa",
+ "expanded_view_description": "Mostra le regole dei filtri con blocchi dettagliati di condizioni e azioni",
+ "if": "Se",
+ "then": "Allora",
+ "match_all_conditions": "tutte corrispondono",
+ "match_any_condition": "una corrisponde",
"and": "e",
"or": "o",
"cancel": "Annulla",
diff --git a/locales/ja/common.json b/locales/ja/common.json
index 7711294e..5834dd51 100644
--- a/locales/ja/common.json
+++ b/locales/ja/common.json
@@ -1183,6 +1183,12 @@
"opaque_warning": "このスクリプトはビジュアルビルダーの外部で編集されました。Sieveスクリプトの直接編集のみ可能です。",
"open_sieve_editor": "Sieveスクリプトエディタを開く",
"fetch_error": "フィルターの読み込みに失敗しました",
+ "expanded_view": "詳細表示",
+ "expanded_view_description": "フィルタールールを条件とアクションのブロックで表示",
+ "if": "条件",
+ "then": "実行",
+ "match_all_conditions": "すべて一致",
+ "match_any_condition": "いずれか一致",
"and": "かつ",
"or": "または",
"cancel": "キャンセル",
diff --git a/locales/nl/common.json b/locales/nl/common.json
index 73b0b0cd..edd39f68 100644
--- a/locales/nl/common.json
+++ b/locales/nl/common.json
@@ -1183,6 +1183,12 @@
"opaque_warning": "Dit script is buiten de visuele builder bewerkt. Alleen Sieve-scriptbewerking is beschikbaar.",
"open_sieve_editor": "Sieve-scripteditor openen",
"fetch_error": "Filters konden niet worden geladen",
+ "expanded_view": "Uitgebreide weergave",
+ "expanded_view_description": "Filterregels weergeven met gedetailleerde voorwaarde- en actieblokken",
+ "if": "Als",
+ "then": "Dan",
+ "match_all_conditions": "alle overeenkomen",
+ "match_any_condition": "een overeenkomt",
"and": "en",
"or": "of",
"cancel": "Annuleren",
diff --git a/locales/pt/common.json b/locales/pt/common.json
index 1a5b55a8..c7f0e5e4 100644
--- a/locales/pt/common.json
+++ b/locales/pt/common.json
@@ -1183,6 +1183,12 @@
"opaque_warning": "Este script foi editado fora do construtor visual. Apenas a edição Sieve bruta está disponível.",
"open_sieve_editor": "Abrir editor Sieve",
"fetch_error": "Falha ao carregar filtros",
+ "expanded_view": "Vista expandida",
+ "expanded_view_description": "Mostrar regras de filtro com blocos detalhados de condições e ações",
+ "if": "Se",
+ "then": "Então",
+ "match_all_conditions": "todas correspondem",
+ "match_any_condition": "uma corresponde",
"and": "e",
"or": "ou",
"cancel": "Cancelar",
diff --git a/stores/settings-store.ts b/stores/settings-store.ts
index 7b85f8d2..84b5fce3 100644
--- a/stores/settings-store.ts
+++ b/stores/settings-store.ts
@@ -119,6 +119,9 @@ interface SettingsState {
sessionTimeout: number; // minutes (0 = never)
trustedSenders: string[]; // Email addresses that can load external content
+ // Filters
+ expandedFilterView: boolean;
+
// Calendar
showTimeInMonthView: boolean;
showWeekNumbers: boolean;
@@ -220,6 +223,9 @@ const DEFAULT_SETTINGS = {
sessionTimeout: 0, // Never
trustedSenders: [] as string[],
+ // Filters
+ expandedFilterView: false,
+
// Calendar
showTimeInMonthView: false,
showWeekNumbers: false,
@@ -308,6 +314,7 @@ export const useSettingsStore = create()(
calendarNotificationsEnabled: state.calendarNotificationsEnabled,
calendarNotificationSound: state.calendarNotificationSound,
calendarInvitationParsingEnabled: state.calendarInvitationParsingEnabled,
+ expandedFilterView: state.expandedFilterView,
showTimeInMonthView: state.showTimeInMonthView,
showWeekNumbers: state.showWeekNumbers,
toolbarPosition: state.toolbarPosition,