Skip to content

Commit b4613d2

Browse files
committed
Feature: filter presets
1 parent 440ba46 commit b4613d2

8 files changed

Lines changed: 176 additions & 0 deletions

File tree

frontend/src/api/validators.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ export const ledgerDataValidator = object({
107107
payees: array(string),
108108
precisions: record(number),
109109
sidebar_links: array(tuple(string, string)),
110+
filter_presets: array(tuple(string, string, string)),
110111
tags: array(string),
111112
upcoming_events_count: number,
112113
user_queries: array(object({ name: string, query_string: string })),
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<script lang="ts">
2+
import { filter_params, fql_filter } from "../stores/filters";
3+
4+
interface Props {
5+
value: string;
6+
label: string;
7+
}
8+
9+
let { value, label }: Props = $props();
10+
11+
// Track if the filter is active (present in the current filter as a substring)
12+
let isActive = $derived($filter_params.filter.includes(value));
13+
14+
function setAdvancedFilter() {
15+
const currentFilter = $filter_params.filter;
16+
17+
// Check if the value is already a substring of the current filter
18+
if (currentFilter.includes(value)) {
19+
// If it's already in the filter, remove it
20+
const newFilter = currentFilter.replace(value, "").trim();
21+
22+
fql_filter.set(newFilter);
23+
} else {
24+
// If it's not in the filter, append it
25+
// If the current filter is empty, just set the value
26+
if (!currentFilter) {
27+
fql_filter.set(value);
28+
} else {
29+
// Otherwise, append space-separated value
30+
fql_filter.set(`${currentFilter} ${value}`);
31+
}
32+
}
33+
}
34+
</script>
35+
36+
<a href="#adv" onclick={setAdvancedFilter} class:active={isActive}>{label}</a>
37+
38+
<style>
39+
a:link {
40+
color: #66c4ff;
41+
}
42+
43+
a:link:hover {
44+
color: #737373;
45+
}
46+
47+
a.active {
48+
font-weight: bold;
49+
text-decoration: underline;
50+
}
51+
</style>
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<script lang="ts">
2+
import { ledgerData } from "../stores";
3+
import AdvancedFilterPresetLink from "./AdvancedFilterPresetLink.svelte";
4+
import TimeFilterPresetLink from "./TimeFilterPresetLink.svelte";
5+
6+
let filter_presets = $derived($ledgerData.filter_presets);
7+
let time_presets = $derived(
8+
filter_presets.filter(([type]) => type === "time"),
9+
);
10+
let advanced_presets = $derived(
11+
filter_presets.filter(([type]) => type === "advanced"),
12+
);
13+
</script>
14+
15+
<div class="filter-presets">
16+
<div class="filter-group">
17+
<span class="filter-presets-label">🕒</span>
18+
<!-- eslint-disable-next-line @typescript-eslint/no-unused-vars -->
19+
{#each time_presets as [_, value, label], index}
20+
{#if index > 0}
21+
<span class="separator">·</span>
22+
{/if}
23+
<TimeFilterPresetLink {value} {label} />
24+
{/each}
25+
</div>
26+
27+
<div class="filter-group">
28+
<span class="filter-presets-label">🎚</span>
29+
<!-- eslint-disable-next-line @typescript-eslint/no-unused-vars -->
30+
{#each advanced_presets as [_, value, label], index}
31+
{#if index > 0}
32+
<span class="separator">·</span>
33+
{/if}
34+
<AdvancedFilterPresetLink {value} {label} />
35+
{/each}
36+
</div>
37+
</div>
38+
39+
<style>
40+
.filter-presets {
41+
display: flex;
42+
justify-content: space-between;
43+
width: 100%;
44+
padding-top: 0.5rem;
45+
padding-right: 0.5rem;
46+
padding-left: 0.5rem;
47+
48+
/* background-color: var(--header-placeholder-background); */
49+
border-top: 1px dotted var(--header-placeholder-background);
50+
}
51+
52+
.filter-group {
53+
display: flex;
54+
gap: 0.5rem;
55+
align-items: center;
56+
}
57+
</style>

frontend/src/sidebar/Header.svelte

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@
33
import router from "../router";
44
import { ledger_title, ledgerData } from "../stores";
55
import FilterForm from "./FilterForm.svelte";
6+
import FilterPresets from "./FilterPresets.svelte";
67
import HeaderIcon from "./HeaderIcon.svelte";
78
import { has_changes } from "./page-title";
89
import PageTitle from "./PageTitle.svelte";
910
1011
let other_ledgers = $derived($ledgerData.other_ledgers);
1112
let has_dropdown = $derived(other_ledgers.length);
13+
let has_filter_presets = $derived($ledgerData.filter_presets.length > 0);
1214
</script>
1315

1416
<header>
@@ -38,6 +40,9 @@
3840
</button>
3941
<span class="spacer"></span>
4042
<FilterForm />
43+
{#if has_filter_presets}
44+
<FilterPresets />
45+
{/if}
4146
</header>
4247

4348
<style>
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<script lang="ts">
2+
import { filter_params, time_filter } from "../stores/filters";
3+
4+
interface Props {
5+
value: string;
6+
label: string;
7+
}
8+
9+
let { value, label }: Props = $props();
10+
11+
let isActive = $derived($filter_params.time === value);
12+
13+
function setTimeFilter() {
14+
if (isActive) {
15+
time_filter.set("");
16+
} else {
17+
time_filter.set(value);
18+
}
19+
}
20+
</script>
21+
22+
<a href="#time" onclick={setTimeFilter} class:active={isActive}>{label}</a>
23+
24+
<style>
25+
a:link {
26+
color: #66c4ff;
27+
}
28+
29+
a:link:hover {
30+
color: #737373;
31+
}
32+
33+
a.active {
34+
font-weight: bold;
35+
text-decoration: underline;
36+
}
37+
</style>

src/fava/core/misc.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,13 @@ def __init__(self, ledger: FavaLedger) -> None:
4040
self.sidebar_links: SidebarLinks = []
4141
#: Upcoming events in the next few days.
4242
self.upcoming_events: Sequence[Event] = []
43+
#: User-defined filter presets.
44+
self.filter_presets: Sequence[tuple[str, str, str]] = []
4345

4446
def load_file(self) -> None: # noqa: D102
4547
custom_entries = self.ledger.all_entries_by_type.Custom
4648
self.sidebar_links = sidebar_links(custom_entries)
49+
self.filter_presets = filter_presets(custom_entries)
4750

4851
self.upcoming_events = upcoming_events(
4952
self.ledger.all_entries_by_type.Event,
@@ -76,6 +79,25 @@ def sidebar_links(custom_entries: Sequence[Custom]) -> SidebarLinks:
7679
]
7780

7881

82+
def filter_presets(
83+
custom_entries: Sequence[Custom],
84+
) -> Sequence[tuple[str, str, str]]:
85+
"""Parse custom entries for filter presets.
86+
87+
They have the following format:
88+
89+
2016-04-01 custom "fava-filter-preset" "time" "month" "Current Month"
90+
2016-04-01 custom "fava-filter-preset" "advanced" "-#tag" "Exclude #tag"
91+
"""
92+
filter_preset_entries = [
93+
entry for entry in custom_entries if entry.type == "fava-filter-preset"
94+
]
95+
return [
96+
(entry.values[0].value, entry.values[1].value, entry.values[2].value)
97+
for entry in filter_preset_entries
98+
]
99+
100+
79101
def upcoming_events(
80102
events: Sequence[Event], max_delta: int
81103
) -> Sequence[Event]:

src/fava/internal_api.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ class LedgerData:
7575
extensions: Sequence[ExtensionDetails]
7676
sidebar_links: Sequence[tuple[str, str]]
7777
other_ledgers: Sequence[tuple[str, str]]
78+
filter_presets: Sequence[tuple[str, str, str]]
7879

7980

8081
def get_errors() -> list[SerialisedError]:
@@ -128,6 +129,7 @@ def get_ledger_data() -> LedgerData:
128129
for (file_slug, ledger) in current_app.config["LEDGERS"].items()
129130
if file_slug != g.beancount_file_slug
130131
],
132+
ledger.misc.filter_presets,
131133
)
132134

133135

tests/__snapshots__/test_internal_api-test_get_ledger_data.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1029,6 +1029,7 @@
10291029
"XYZ": 0
10301030
},
10311031
"sidebar_links": [],
1032+
"filter_presets": [],
10321033
"tags": [
10331034
"test",
10341035
"trip-chicago-2015",

0 commit comments

Comments
 (0)