Keep Postvak and Meelezen replies as concepts #468

Open
robbertbos wants to merge 2 commits from postvak-reply-drafts into main
Owner

A reply typed in the Postvak or Meelezen lived in localStorage only. Once its message was marked read, the row closed and took the composer with it, so the text was unreachable. It is now a server-side concept, like a card reply, listed in "Concepten" and finished from there.

Backend

  • A Postvak reply concept is a card-less outgoing_posts draft carrying its thread in root_id (the column a scheduled Postvak reply already uses). One per thread: new partial unique index ux_outgoing_one_reply_draft_per_thread on (user_id, account_id, root_id) WHERE state = 'draft' AND card_id IS NULL AND root_id IS NOT NULL. Rolling-safe: the app creates no such rows today.
  • GET/PUT/DELETE /api/mm/reply-draft (tag drafts). Saving makes no Mattermost call; the thread is checked at send time, as before. A save that loses the race to create the row writes onto the winner's.
  • DraftRead now carries account_id, channel_id and root_id.
  • The new-message draft routes skip a card-less draft with a root_id. Opened in the new-message composer it would be sent at channel level, outside its thread.

Frontend

  • The Postvak composer keys its concept by thread (postvak:<account>:<root>), so it answers the same concept whichever message of the thread it was opened under. useServerDraftSync takes the owner (card or thread) instead of a card id.
  • Concepten shows the row as "Antwoord in ". Clicking it opens PostvakReplyDialog: the thread as context, then the same ReplyComposer. Sending or scheduling there consumes the concept; the discard goes through the reply-draft route.
  • When "Tot hier gelezen" closes a row while its composer holds text, a notification says "Je antwoord is bewaard in Concepten." with a "Bekijken" link.

Checked

  • Backend: full suite, 100% coverage; new test_postvak_reply_draft.py plus a migration check on the index predicate.
  • Frontend: typecheck, eslint, full vitest (1757 tests).
  • In the preview with Playwright: typed a reply in the Postvak and clicked "Tot hier gelezen". The concept was stored on the thread, showed in Concepten as "Antwoord in Town Square · Digitale Overheid", and reopening it showed the thread with the text in the composer. The dev mock keeps no read cursors, so the row does not leave there; the notification is covered by a unit test instead.

Notes

  • For a message outside any thread the concept is keyed on the message itself. In a channel without Collapsed Reply Threads a reply can show as a plain channel message, and it is then keyed on that reply rather than its root. Sending still lands in the right thread, because the send endpoint derives the root from the post.
  • Replies started before this change stay in localStorage under the old post:<id> key and are not migrated.
  • This conflicts with #467 on CHANGELOG.md and frontend/src/content/releases/unreleased.md (both new under Unreleased). Whichever merges second needs a rebase.
A reply typed in the Postvak or Meelezen lived in localStorage only. Once its message was marked read, the row closed and took the composer with it, so the text was unreachable. It is now a server-side concept, like a card reply, listed in "Concepten" and finished from there. ## Backend - A Postvak reply concept is a card-less `outgoing_posts` draft carrying its thread in `root_id` (the column a scheduled Postvak reply already uses). One per thread: new partial unique index `ux_outgoing_one_reply_draft_per_thread` on `(user_id, account_id, root_id) WHERE state = 'draft' AND card_id IS NULL AND root_id IS NOT NULL`. Rolling-safe: the app creates no such rows today. - `GET/PUT/DELETE /api/mm/reply-draft` (tag `drafts`). Saving makes no Mattermost call; the thread is checked at send time, as before. A save that loses the race to create the row writes onto the winner's. - `DraftRead` now carries `account_id`, `channel_id` and `root_id`. - The new-message draft routes skip a card-less draft with a `root_id`. Opened in the new-message composer it would be sent at channel level, outside its thread. ## Frontend - The Postvak composer keys its concept by thread (`postvak:<account>:<root>`), so it answers the same concept whichever message of the thread it was opened under. `useServerDraftSync` takes the owner (card or thread) instead of a card id. - Concepten shows the row as "Antwoord in <kanaal>". Clicking it opens `PostvakReplyDialog`: the thread as context, then the same `ReplyComposer`. Sending or scheduling there consumes the concept; the discard goes through the reply-draft route. - When "Tot hier gelezen" closes a row while its composer holds text, a notification says "Je antwoord is bewaard in Concepten." with a "Bekijken" link. ## Checked - Backend: full suite, 100% coverage; new `test_postvak_reply_draft.py` plus a migration check on the index predicate. - Frontend: typecheck, eslint, full vitest (1757 tests). - In the preview with Playwright: typed a reply in the Postvak and clicked "Tot hier gelezen". The concept was stored on the thread, showed in Concepten as "Antwoord in Town Square · Digitale Overheid", and reopening it showed the thread with the text in the composer. The dev mock keeps no read cursors, so the row does not leave there; the notification is covered by a unit test instead. ## Notes - For a message outside any thread the concept is keyed on the message itself. In a channel without Collapsed Reply Threads a reply can show as a plain channel message, and it is then keyed on that reply rather than its root. Sending still lands in the right thread, because the send endpoint derives the root from the post. - Replies started before this change stay in localStorage under the old `post:<id>` key and are not migrated. - This conflicts with #467 on `CHANGELOG.md` and `frontend/src/content/releases/unreleased.md` (both new under Unreleased). Whichever merges second needs a rebase.
A reply typed in the Postvak lived in localStorage only. Once its message
was marked read the row closed, and with it the only place the text
could be reached. Card replies never had this problem: their draft is an
outbox row, listed in Concepten.

Give a Postvak reply the same: a card-less draft row that carries the
thread it answers in root_id, one per thread (a partial unique index on
user, account and root), behind GET/PUT/DELETE /api/mm/reply-draft.
Saving makes no Mattermost call; the thread is checked when the reply is
sent, as before. DraftRead now carries account_id, channel_id and
root_id so Concepten can tell this concept apart and reopen it.

The new-message routes skip a card-less draft with a root_id: opened as
a new message it would be posted outside its thread.
Keep Postvak replies as concepts and reopen them from Concepten
All checks were successful
CI / pre-commit (pull_request) Successful in 2m10s
CI / release-scripts (pull_request) Successful in 17s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 53s
security-scan / Python SAST (bandit) (pull_request) Successful in 32s
security-scan / JS SCA (npm audit) (pull_request) Successful in 31s
CI / frontend-test (pull_request) Successful in 4m49s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 26s
security-scan / SBOM (trivy) (pull_request) Successful in 17s
test-build / build (backend) (pull_request) Successful in 2m14s
test-build / build (frontend) (pull_request) Successful in 2m11s
test-build / build (pull_request) Successful in 0s
CI / e2e (pull_request) Successful in 15m18s
CI / backend-test (pull_request) Successful in 17m1s
c3f635964a
The Postvak composer now mirrors what is typed to the reply-draft
endpoint, keyed by the thread it answers, as the card composer does for
its card. useServerDraftSync takes the owner of the concept - a card or
a thread - instead of a card id.

In Concepten such a concept reads "Antwoord in <kanaal>" and opens a
dialog with its thread above the same composer; sending or scheduling
there consumes it, and discarding goes through the reply-draft route.
When "Tot hier gelezen" closes a row while its composer holds text, a
notification says the answer is in Concepten, with a link there.
All checks were successful
CI / pre-commit (pull_request) Successful in 2m10s
Required
Details
CI / release-scripts (pull_request) Successful in 17s
Required
Details
security-scan / Python SCA (pip-audit) (pull_request) Successful in 53s
Required
Details
security-scan / Python SAST (bandit) (pull_request) Successful in 32s
Required
Details
security-scan / JS SCA (npm audit) (pull_request) Successful in 31s
Required
Details
CI / frontend-test (pull_request) Successful in 4m49s
Required
Details
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 26s
Required
Details
security-scan / SBOM (trivy) (pull_request) Successful in 17s
Required
Details
test-build / build (backend) (pull_request) Successful in 2m14s
Required
Details
test-build / build (frontend) (pull_request) Successful in 2m11s
Required
Details
test-build / build (pull_request) Successful in 0s
Required
Details
CI / e2e (pull_request) Successful in 15m18s
Required
Details
CI / backend-test (pull_request) Successful in 17m1s
Required
Details
This pull request has changes conflicting with the target branch.
  • CHANGELOG.md
View command line instructions

Manual merge helper

Use this merge commit message when completing the merge manually.

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin postvak-reply-drafts:postvak-reply-drafts
git switch postvak-reply-drafts
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
robbertbos/waggle!468
No description provided.