From c4914fe587b046c2e2789841d4410e3b6de7ea6b Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Wed, 22 Jul 2026 08:39:20 -0400 Subject: [PATCH] =?UTF-8?q?m4.5:=20inline=20#tags=20=E2=80=94=20hashtags?= =?UTF-8?q?=20in=20a=20note=20become=20labels=20(two-way=20sync)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fast, cross-device labelling: type #groceries in a note and it becomes the "groceries" label. The body is the source of truth for tag-labels; manual picker labels stay independent (rule 28 — additive). - note_labels.via_tag (migration 0013) marks tag-sourced attachments. - parse_tags(): #tag at start-of-body or after whitespace, needs a letter (so #2024, URL #frags, mid#word are ignored). unit-tested. - _reconcile_tags() on create + body-update: attach labels for current #tags (find-or-create, case-insensitive), detach tag-labels whose tag was removed; never touches manual (via_tag=false) rows. - label picker (set_note_labels + editor onLabelsChange) now preserves tag-labels on save, so a picker action can't strip a label the #tag still mandates. - serialize via_tag; card/editor chips render tag-labels as "#name", and the editor hides the × on them (remove by editing the tag text). - LabelPicker builds manual NoteLabels (via_tag:false). Fourth and final item of M4.5. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm --- alembic/versions/0013_note_label_via_tag.py | 26 +++++++ frontend/src/components/LabelPicker.vue | 11 ++- frontend/src/components/NoteCard.vue | 2 +- frontend/src/components/NoteEditor.vue | 12 ++- frontend/src/stores/notes.ts | 3 + src/thoughtsync/models/label.py | 5 +- src/thoughtsync/notes.py | 84 +++++++++++++++++++-- tests/test_notes.py | 11 +++ 8 files changed, 137 insertions(+), 17 deletions(-) create mode 100644 alembic/versions/0013_note_label_via_tag.py diff --git a/alembic/versions/0013_note_label_via_tag.py b/alembic/versions/0013_note_label_via_tag.py new file mode 100644 index 0000000..f3f1c09 --- /dev/null +++ b/alembic/versions/0013_note_label_via_tag.py @@ -0,0 +1,26 @@ +"""note_labels.via_tag + +Revision ID: 0013 +Revises: 0012 +Create Date: 2026-07-22 +""" +from alembic import op +import sqlalchemy as sa + +revision = "0013" +down_revision = "0012" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # Marks a note↔label attachment that came from a body #tag (vs. the picker). + # Existing attachments default to False = manual, which is correct. + op.add_column( + "note_labels", + sa.Column("via_tag", sa.Boolean(), nullable=False, server_default=sa.false()), + ) + + +def downgrade() -> None: + op.drop_column("note_labels", "via_tag") diff --git a/frontend/src/components/LabelPicker.vue b/frontend/src/components/LabelPicker.vue index 07f4d9e..e109d02 100644 --- a/frontend/src/components/LabelPicker.vue +++ b/frontend/src/components/LabelPicker.vue @@ -1,9 +1,12 @@