Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion frontend/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,7 @@ export async function delete_document(filename: string): Promise<boolean> {
}

/**
* Save an array of entries.
* Save an array of entries, notify and reload.
* @param entries - an array of entries to save to the Beancount file.
*/
export async function save_entries(
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/api/validators.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { account_hierarchy_validator } from "../charts/hierarchy.ts";
import { charts_validator } from "../charts/index.ts";
import { entryBaseValidator } from "../entries/index.ts";
import { entryValidator } from "../entries/index.ts";
import type { ValidationT } from "../lib/validation.ts";
import {
array,
Expand Down Expand Up @@ -136,7 +136,7 @@ export const commodities_validator = array(
export type Commodities = ValidationT<typeof commodities_validator>;

export const context_validator = object({
entry: entryBaseValidator,
entry: entryValidator,
balances_before: optional(record(array(string))),
balances_after: optional(record(array(string))),
});
Expand Down
22 changes: 16 additions & 6 deletions frontend/src/editor/DeleteButton.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,25 @@
import { _ } from "../i18n.ts";

interface Props {
deleting: boolean;
onDelete: () => void;
ondelete: () => Promise<void>;
}

let { deleting, onDelete }: Props = $props();
let { ondelete }: Props = $props();

let buttonContent = $derived(deleting ? _("Deleting…") : _("Delete"));
let deleting = $state(false);

let content = $derived(deleting ? _("Deleting…") : _("Delete"));

async function onclick() {
deleting = true;
try {
await ondelete();
} finally {
deleting = false;
}
}
</script>

<button type="button" class="muted" onclick={onDelete} title={_("Delete")}>
{buttonContent}
<button type="button" class="muted" {onclick} title={content}>
{content}
</button>
78 changes: 67 additions & 11 deletions frontend/src/editor/SliceEditor.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,22 @@
<!--
@component
Edit the source slice of an entry or duplicate it.
-->
<script lang="ts">
import { delete_source_slice, put_source_slice } from "../api/index.ts";
import {
delete_source_slice,
put_source_slice,
save_entries,
} from "../api/index.ts";
import { attach_editor } from "../codemirror/dom.ts";
import type { CodemirrorBeancount } from "../codemirror/types.ts";
import {
type EditableEntry,
type Entry,
is_editable,
} from "../entries/index.ts";
import EntrySvelte from "../entry-forms/Entry.svelte";
import { todayAsString } from "../format.ts";
import { _ } from "../i18n.ts";
import { notify_err } from "../notifications.ts";
import { router } from "../router.ts";
Expand All @@ -11,13 +26,15 @@
import SaveButton from "./SaveButton.svelte";

interface Props {
entry: Entry;
slice: string;
entry_hash: string;
sha256sum: string;
codemirror_beancount: CodemirrorBeancount;
}

let {
entry,
slice,
entry_hash = $bindable(),
sha256sum = $bindable(),
Expand All @@ -28,19 +45,20 @@
// svelte-ignore state_referenced_locally
const initial_slice = slice;

let currentSlice = $state(initial_slice);
let changed = $derived(currentSlice !== initial_slice);
let current_slice = $state(initial_slice);
let changed = $derived(current_slice !== initial_slice);

let duplicated_entry = $state.raw<EditableEntry>();

let saving = $state(false);
let deleting = $state(false);

async function save(event?: SubmitEvent) {
event?.preventDefault();
saving = true;
try {
sha256sum = await put_source_slice({
entry_hash,
source: currentSlice,
source: current_slice,
sha256sum,
});
if ($reloadAfterSavingEntrySlice) {
Expand All @@ -54,8 +72,19 @@
}
}

async function deleteSlice() {
deleting = true;
async function save_duplicated_entry(event: SubmitEvent) {
event.preventDefault();
try {
if (duplicated_entry != null) {
await save_entries([duplicated_entry]);
}
router.close_overlay();
} finally {
duplicated_entry = undefined;
}
}

async function delete_slice() {
try {
await delete_source_slice({ entry_hash, sha256sum });
entry_hash = "";
Expand All @@ -65,16 +94,14 @@
router.close_overlay();
} catch (error) {
notify_err(error, (err) => `Deleting failed: ${err.message}`);
} finally {
deleting = false;
}
}

// svelte-ignore state_referenced_locally
const editor = codemirror_beancount.init_beancount_editor(
initial_slice,
(state) => {
currentSlice = state.sliceDoc();
current_slice = state.sliceDoc();
},
[
{
Expand All @@ -96,18 +123,47 @@
<form onsubmit={save} class="flex-column">
<div class="editor" {@attach attach_editor(editor)}></div>
<div class="flex-row">
{#if is_editable(entry)}
<button
type="button"
class="muted"
onclick={() => {
if (duplicated_entry == null) {
duplicated_entry = entry.set("date", todayAsString());
} else {
duplicated_entry = undefined;
}
}}
>
{_("Duplicate")}
</button>
{/if}
<span class="spacer"></span>
<label>
<input type="checkbox" bind:checked={$reloadAfterSavingEntrySlice} />
<span>{_("reload")}</span>
</label>
<DeleteButton {deleting} onDelete={deleteSlice} />
<DeleteButton ondelete={delete_slice} />
<SaveButton {changed} {saving} />
</div>
</form>
{#if duplicated_entry}
<form onsubmit={save_duplicated_entry} class="flex-column">
<h3>{_("Add")} {_(duplicated_entry.t)}</h3>
<EntrySvelte bind:entry={duplicated_entry} />
<div class="flex-row">
<span class="spacer"></span>
<button type="submit">{_("Save")}</button>
</div>
</form>
{/if}

<style>
.editor {
border: 1px solid var(--sidebar-border);
}

h3 {
margin-top: 0.5em;
}
</style>
34 changes: 15 additions & 19 deletions frontend/src/entries/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,21 +58,6 @@ export class Posting {
);
}

/** The properties that all entries share. */
export interface EntryBaseAttributes {
t: string;
meta: EntryMetadata;
date: string;
entry_hash: string;
}

export const entryBaseValidator = object<EntryBaseAttributes>({
t: string,
meta: EntryMetadata.validator,
date: string,
entry_hash: string,
});

const string_array_validator = array(string);
const optional_string_array_validator = optional(string_array_validator);

Expand Down Expand Up @@ -101,6 +86,8 @@ abstract class EntryBase<T extends string> {
}

/** Set a property and return an updated copy. */
set(key: "date", value: string): this;
set<K extends keyof typeof this>(key: K, value: (typeof this)[K]): this;
set<K extends keyof typeof this>(key: K, value: (typeof this)[K]): this {
const copy = this.clone();
copy[key] = value;
Expand All @@ -109,10 +96,7 @@ abstract class EntryBase<T extends string> {

/** Set the value for a key and return an updated copy. */
set_meta(key: string, value: MetadataValue): this {
const copy = this.clone();
// @ts-expect-error We can mutate it as we just created it and noone has access yet.
copy.meta = this.meta.set(key, value);
return copy;
return this.set("meta", this.meta.set(key, value));
}

/** Check whether the given entry is marked as duplicate (used in imports). */
Expand Down Expand Up @@ -638,3 +622,15 @@ export const entryValidator = tagged_union("t", {

/** A Beancount entry, currently only supports some of the types. */
export type Entry = ValidationT<typeof entryValidator>;

/** The types that the entry component supports. */
export type EditableEntry = Balance | Note | Transaction;

/** Type guard for editable entry. */
export function is_editable(entry: Entry): entry is EditableEntry {
return (
entry instanceof Balance ||
entry instanceof Note ||
entry instanceof Transaction
);
}
2 changes: 1 addition & 1 deletion frontend/src/entry-forms/Entry.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
{:else if entry instanceof Transaction}
<TransactionSvelte bind:entry />
{:else}
Entry type unsupported for editing.
Entry type {entry.t} is not supported for editing.
{/if}
</div>

Expand Down
19 changes: 12 additions & 7 deletions frontend/src/modals/AddEntry.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
<script lang="ts">
import { save_entries } from "../api/index.ts";
import { Balance, Note, Transaction } from "../entries/index.ts";
import {
Balance,
type EditableEntry,
Note,
Transaction,
} from "../entries/index.ts";
import Entry from "../entry-forms/Entry.svelte";
import { todayAsString } from "../format.ts";
import { _ } from "../i18n.ts";
Expand All @@ -17,9 +22,7 @@
] as const;

// For the first entry to be added, use today as the default date.
let entry: Transaction | Balance | Note = $state.raw(
Transaction.empty(todayAsString()),
);
let entry = $state.raw<EditableEntry>(Transaction.empty(todayAsString()));

async function submit(event: SubmitEvent) {
event.preventDefault();
Expand All @@ -46,14 +49,12 @@
type="button"
class:muted={!(entry instanceof Cls)}
onclick={() => {
// when switching between entry types, keep the date.
// When switching between entry types, keep the date.
entry = Cls.empty(entry.date);
}}
>
{displayName}
</button>
<!-- eslint-disable-next-line svelte/no-useless-mustaches -->
{" "}
{/each}
</h3>
<Entry bind:entry />
Expand All @@ -71,6 +72,10 @@
<style>
h3 {
margin: 0;

button {
margin-left: 0.25em;
}
}

label span {
Expand Down
1 change: 1 addition & 0 deletions frontend/src/modals/Context.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
<p>Loading entry slice...</p>
{:then [{ slice, sha256sum }, codemirror_beancount]}
<SliceEditor
{entry}
{entry_hash}
{slice}
{sha256sum}
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/modals/EntryContextLocation.svelte
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<script lang="ts">
import type { EntryBaseAttributes } from "../entries/index.ts";
import type { Entry } from "../entries/index.ts";
import { urlForSource } from "../helpers.ts";
import { _ } from "../i18n.ts";

interface Props {
entry: EntryBaseAttributes;
entry: Entry;
}

let { entry }: Props = $props();
Expand Down
Loading
Loading