From 15f9cf8ceb4570dec6cf9d597bb976379c41f2c5 Mon Sep 17 00:00:00 2001 From: Tyler Schicke Date: Fri, 23 Jun 2023 14:48:40 -0700 Subject: [PATCH 1/4] Expose SliceEditor as a custom svelte component This allows extensions to use the SliceEditor in their template html. This requires changing the json object that the components provide to be the entire props object instead of just the data field. --- frontend/src/svelte-custom-elements.ts | 16 ++++++++++++---- src/fava/templates/account.html | 2 +- src/fava/templates/balance_sheet.html | 14 ++++++++------ src/fava/templates/income_statement.html | 16 +++++++++------- src/fava/templates/trial_balance.html | 16 +++++++++------- 5 files changed, 39 insertions(+), 25 deletions(-) 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/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('')) }} From 37c0235878fdd51c12aad194130c568c5fe03229 Mon Sep 17 00:00:00 2001 From: Tyler Schicke Date: Sat, 1 Jul 2023 16:14:08 -0700 Subject: [PATCH 2/4] Type-checking and Linting --- src/fava/ext/batch_edit/BatchEdit.js | 19 ++++++ src/fava/ext/batch_edit/__init__.py | 68 +++++++++++++++++++ .../ext/batch_edit/templates/BatchEdit.html | 18 +++++ 3 files changed, 105 insertions(+) create mode 100644 src/fava/ext/batch_edit/BatchEdit.js create mode 100644 src/fava/ext/batch_edit/__init__.py create mode 100644 src/fava/ext/batch_edit/templates/BatchEdit.html 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..7283880b2 --- /dev/null +++ b/src/fava/ext/batch_edit/__init__.py @@ -0,0 +1,68 @@ +"""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.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 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 %} From e812027b35424ebdc4bb092cffa17e114b43d9d6 Mon Sep 17 00:00:00 2001 From: Tyler Schicke Date: Sat, 1 Jul 2023 16:19:00 -0700 Subject: [PATCH 3/4] Update README --- src/fava/help/extensions.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 From 1f5cf41bfad613bb6d6273daf80857b8d25b961f Mon Sep 17 00:00:00 2001 From: Tyler Schicke Date: Sat, 1 Jul 2023 17:54:44 -0700 Subject: [PATCH 4/4] Fix linter error --- src/fava/ext/batch_edit/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/fava/ext/batch_edit/__init__.py b/src/fava/ext/batch_edit/__init__.py index 7283880b2..31ec05a97 100644 --- a/src/fava/ext/batch_edit/__init__.py +++ b/src/fava/ext/batch_edit/__init__.py @@ -10,6 +10,7 @@ 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 @@ -55,7 +56,7 @@ def source_slices(self, query: str) -> list[dict[str, str]]: for tx_id in transaction_ids[:20]: entry = entries[tx_id] # Skip generated entries - if entry.flag == "S": + if isinstance(entry, Transaction) and entry.flag == "S": continue source_slice, sha256sum = get_entry_slice(entry) results.append(