diff --git a/frontend/src/svelte-custom-elements.ts b/frontend/src/svelte-custom-elements.ts index 976731673..d606a6da9 100644 --- a/frontend/src/svelte-custom-elements.ts +++ b/frontend/src/svelte-custom-elements.ts @@ -5,9 +5,14 @@ import type { SvelteComponent } from "svelte"; import ChartSwitcher from "./charts/ChartSwitcher.svelte"; +import SliceEditor from "./editor/SliceEditor.svelte"; -const components = new Map>([ +const components = new Map< + string, + typeof SvelteComponent> +>([ ["charts", ChartSwitcher], + ["slice-editor", SliceEditor], ]); /** @@ -17,7 +22,7 @@ const components = new Map>([ * of the valid values in the Map above. */ export class SvelteCustomElement extends HTMLElement { - component?: SvelteComponent<{ data?: unknown }>; + component?: SvelteComponent>; connectedCallback(): void { if (this.component) { @@ -31,10 +36,13 @@ export class SvelteCustomElement extends HTMLElement { if (!Cls) { throw new Error("Invalid component"); } - const props: { data?: unknown } = {}; + const props: Record = {}; const script = this.querySelector("script"); if (script && script.type === "application/json") { - props.data = JSON.parse(script.innerHTML); + const data: unknown = JSON.parse(script.innerHTML); + if (data instanceof Object) { + Object.assign(props, data); + } } this.component = new Cls({ target: this, props }); } diff --git a/src/fava/ext/batch_edit/BatchEdit.js b/src/fava/ext/batch_edit/BatchEdit.js new file mode 100644 index 000000000..5bdb591af --- /dev/null +++ b/src/fava/ext/batch_edit/BatchEdit.js @@ -0,0 +1,19 @@ +export default { + async runQuery() { + const queryStr = document.getElementById("batch_edit_query").value; + console.log(queryStr); + if (!queryStr) { + return; + } + let searchParams = new URLSearchParams(window.location.search); + searchParams.set("query", queryStr); + window.location.search = searchParams.toString(); + return; + }, + onExtensionPageLoad() { + const submitQuery = document.getElementById("batch_query_submit"); + submitQuery.addEventListener("click", () => { + this.runQuery(); + }); + }, +}; diff --git a/src/fava/ext/batch_edit/__init__.py b/src/fava/ext/batch_edit/__init__.py new file mode 100644 index 000000000..31ec05a97 --- /dev/null +++ b/src/fava/ext/batch_edit/__init__.py @@ -0,0 +1,69 @@ +"""Batch editor extension for Fava. + +This is a simple batch editor that allows a batch of 20 entries +retrieved with a BQL query to be edited on the same page + +There is currently a limitation where each entry needs to be saved +individually +""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +from fava.beans.abc import Transaction +from fava.beans.funcs import hash_entry +from fava.context import g +from fava.core.file import get_entry_slice +from fava.ext import FavaExtensionBase +from fava.helpers import FavaAPIError + +if TYPE_CHECKING: # pragma: no cover + from fava.beans.abc import Directive + + +class BatchEdit(FavaExtensionBase): + """Extension page that allows basic batch editing of entries.""" + + report_title = "Batch Editor" + + has_js_module = True + + def get_entries(self, entry_hashes: list[str]) -> dict[str, Directive]: + """Find a set of entries. + + Arguments: + entry_hashes: Hashes of the entries. + + Returns: + A dictionary of { entry_id: entry } for each given entry hash that is found + """ + entries_set = set(entry_hashes) + hashed_entries = [(hash_entry(e), e) for e in g.filtered.entries] + return { + key: entry for key, entry in hashed_entries if key in entries_set + } + + def source_slices(self, query: str) -> list[dict[str, str]]: + contents, _types, rows = self.ledger.query_shell.execute_query( + g.filtered.entries, f"SELECT distinct id WHERE {query}" + ) + if contents and "ERROR" in contents: + raise FavaAPIError(contents) + + transaction_ids = [row.id for row in rows] + entries = self.get_entries(transaction_ids) + results = [] + for tx_id in transaction_ids[:20]: + entry = entries[tx_id] + # Skip generated entries + if isinstance(entry, Transaction) and entry.flag == "S": + continue + source_slice, sha256sum = get_entry_slice(entry) + results.append( + { + "slice": source_slice, + "entry_hash": tx_id, + "sha256sum": sha256sum, + } + ) + return results diff --git a/src/fava/ext/batch_edit/templates/BatchEdit.html b/src/fava/ext/batch_edit/templates/BatchEdit.html new file mode 100644 index 000000000..274653ea6 --- /dev/null +++ b/src/fava/ext/batch_edit/templates/BatchEdit.html @@ -0,0 +1,18 @@ +
+

Query

+ + + +
+
+{% set query = request.args.get('query') %} +{% if query %} +
+ {% for slice in extension.source_slices(query) %} +
+ {{slice["entry_hash"]}} + +
+ {% endfor %} +
+{% endif %} diff --git a/src/fava/help/extensions.md b/src/fava/help/extensions.md index d67cc14f2..be17a1163 100644 --- a/src/fava/help/extensions.md +++ b/src/fava/help/extensions.md @@ -13,7 +13,9 @@ Extensions may also contain a report - this is detected when the extension's class has a `report_title` attribute. The template for the report should be in a `templates` subdirectory with a report matching the class's name. For example, check out `fava.ext.portfolio_list` which has its template located at -`fava/ext/portfolio_list/templates/PortfolioList.html`. +`fava/ext/portfolio_list/templates/PortfolioList.html`, or `fava.ext.batch_edit` +which has its template located at +`fava/ext/batch_edit/templates/BatchEdit.html`. Finally, extensions may contain a Javascript module to be loaded in the frontend. The module should be in a Javascript file matching the class's name diff --git a/src/fava/templates/account.html b/src/fava/templates/account.html index 1808ff28f..68324afd5 100644 --- a/src/fava/templates/account.html +++ b/src/fava/templates/account.html @@ -23,7 +23,7 @@ {% endfor %} {% endif %} - +
diff --git a/src/fava/templates/balance_sheet.html b/src/fava/templates/balance_sheet.html index 029b42618..cd1272a88 100644 --- a/src/fava/templates/balance_sheet.html +++ b/src/fava/templates/balance_sheet.html @@ -1,12 +1,14 @@ {% import '_tree_table.html' as tree_table with context %} {% set root_tree_closed = g.filtered.root_tree_closed %} diff --git a/src/fava/templates/income_statement.html b/src/fava/templates/income_statement.html index 9ecb80431..69151bfd0 100644 --- a/src/fava/templates/income_statement.html +++ b/src/fava/templates/income_statement.html @@ -5,13 +5,15 @@ {% set invert = ledger.fava_options.invert_income_liabilities_equity %}
diff --git a/src/fava/templates/trial_balance.html b/src/fava/templates/trial_balance.html index b2dc2bf77..6e01d86cc 100644 --- a/src/fava/templates/trial_balance.html +++ b/src/fava/templates/trial_balance.html @@ -1,13 +1,15 @@ {% import '_tree_table.html' as tree_table with context %} {{ tree_table.tree(g.filtered.root_tree.get('')) }}