diff --git a/core/src/local/recur.rs b/core/src/local/recur.rs index a5fe542..38069f6 100644 --- a/core/src/local/recur.rs +++ b/core/src/local/recur.rs @@ -74,6 +74,19 @@ mod tests { use super::*; use chrono::TimeZone; + #[test] + fn the_rules_are_the_shared_fixtures() { + let fixture: serde_json::Value = + serde_json::from_str(include_str!("../../testdata/grammar.json")).expect("parses"); + let want: Vec<&str> = fixture["recurrences"] + .as_array() + .expect("a list") + .iter() + .map(|r| r.as_str().expect("a string")) + .collect(); + assert_eq!(RECURRENCES.to_vec(), want); + } + fn utc(y: i32, m: u32, d: u32, h: u32, min: u32) -> DateTime { Utc.with_ymd_and_hms(y, m, d, h, min, 0).unwrap() } diff --git a/core/testdata/grammar.json b/core/testdata/grammar.json index 0d4c9d8..c0173ee 100644 --- a/core/testdata/grammar.json +++ b/core/testdata/grammar.json @@ -5,6 +5,7 @@ "A difference between any two implementations is a note that changes shape when it syncs. Add a case here, not to one suite.", "Lives under core/ because core is the definition, and because core/ is in the desktop, Android and server file sets, so changing a case reruns every suite that reads it.", "display_titles: a note's name is its first line that says anything once a task marker is stripped, cut to 200 characters (not bytes). Lines split on \\n alone, as the grammar does everywhere.", + "recurrences: the repeat rules a reminder can carry, as stored. The server, the core and the web's editor each hold the list; Android's picker pins it by hand, as it does the tint.", "Tag cases are plain text on purpose. The web's renderer lets `code` and **bold** claim a `#` first, which the derivers do not; that is a rendering choice, not grammar, and is tested where it lives." ], @@ -81,6 +82,8 @@ { "body": "ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "title": "éééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé" } ], + "recurrences": ["daily", "weekly", "monthly", "yearly"], + "tint": { "keys": ["red", "orange", "yellow", "green", "teal", "blue", "purple", "pink", "gray"], "hashes": [ diff --git a/frontend/src/components/NoteEditor.vue b/frontend/src/components/NoteEditor.vue index 9487a0d..72a6be2 100644 --- a/frontend/src/components/NoteEditor.vue +++ b/frontend/src/components/NoteEditor.vue @@ -9,6 +9,7 @@ import ReminderActions from "./ReminderActions.vue"; import LabelPicker from "./LabelPicker.vue"; import LinkPreview from "./LinkPreview.vue"; import { formatShortDateTime, fromLocalInput, toLocalInput } from "../notes/datetime"; +import { RECURRENCES } from "../notes/recurrence"; import { takeMorphOrigin } from "../composables/useEditorMorph"; import { prefersReducedMotion } from "../composables/useReducedMotion"; import type { Note, NoteLabel, NoteRevision } from "../stores/notes"; @@ -749,10 +750,7 @@ function revPreview(rev: NoteRevision): string { @change="onRecurrenceChange" > - - - - + diff --git a/frontend/src/notes/grammar.test.ts b/frontend/src/notes/grammar.test.ts index 5b34eb4..0cbe36b 100644 --- a/frontend/src/notes/grammar.test.ts +++ b/frontend/src/notes/grammar.test.ts @@ -8,6 +8,7 @@ import { describe, expect, it } from "vitest"; import fixture from "../../../core/testdata/grammar.json"; import { DERIVED_TINT_KEYS, derivedTint, resolveLabelColor, tintHash } from "./colors"; import { parseInline, parseTaskLine, renderTaskLine } from "./markdown"; +import { RECURRENCES } from "./recurrence"; describe("checklist lines", () => { it.each(fixture.task_lines)("$line", ({ line, item }) => { @@ -52,3 +53,9 @@ describe("derived tag colour", () => { expect(resolveLabelColor({ name })).toBe(color); }); }); + +describe("recurrences", () => { + it("offers the stored rules every surface holds", () => { + expect(RECURRENCES.map((r) => r.value)).toEqual(fixture.recurrences); + }); +}); diff --git a/frontend/src/notes/recurrence.ts b/frontend/src/notes/recurrence.ts new file mode 100644 index 0000000..f059ac1 --- /dev/null +++ b/frontend/src/notes/recurrence.ts @@ -0,0 +1,9 @@ +// The repeat rules a reminder can carry, as stored, and what the editor calls each. +// The server (REMINDER_RECURRENCES) and the core (recur::RECURRENCES) hold the same +// list, and all three run the `recurrences` case in core/testdata/grammar.json. +export const RECURRENCES: ReadonlyArray<{ value: string; label: string }> = [ + { value: "daily", label: "Daily" }, + { value: "weekly", label: "Weekly" }, + { value: "monthly", label: "Monthly" }, + { value: "yearly", label: "Yearly" }, +]; diff --git a/tests/test_grammar_fixture.py b/tests/test_grammar_fixture.py index dd3c23a..84dec3d 100644 --- a/tests/test_grammar_fixture.py +++ b/tests/test_grammar_fixture.py @@ -14,6 +14,7 @@ import pytest from inkwell.notes.checklist import parse_items, render_item from inkwell.notes.helpers import derive_display_title +from inkwell.notes.recurrence import REMINDER_RECURRENCES from inkwell.notes.tags import parse_tags, split_body_tags FIXTURE = json.loads( @@ -47,3 +48,7 @@ def test_lifts(case): @pytest.mark.parametrize("case", FIXTURE["display_titles"], ids=lambda c: repr(c["body"][:40])) def test_display_titles(case): assert derive_display_title(case["body"]) == case["title"] + + +def test_recurrences(): + assert REMINDER_RECURRENCES == set(FIXTURE["recurrences"])