Simplify the Subject move - #1361
Draft
JeroenDeDauw wants to merge 6 commits into
Draft
Conversation
Fixes #1338 A Subject created in flow from a relation field is stored on the page holding the Subject whose relation was being filled in, which is often not where it belongs. Relocating one meant deleting it and recreating it elsewhere, which mints a new id and breaks every relation pointing at the old one. ## What it does Every Subject row on the Data tab gains a Move action, before Delete in both the button strip and the overflow menu. It opens a dialog holding a page lookup, a checkbox for making the Subject the target page's Main Subject, and the notices each choice earns. The Subject keeps its id, so the relations targeting it keep resolving and the subject-page index follows it to its new page. A page that does not exist yet can be named in the lookup; it is created when the move is confirmed, so an abandoned dialog leaves nothing behind. `POST /neowiki/v0/subject/{subjectId}/move` is the write surface. ## Decisions worth challenging **The source page is written first.** A move is two page edits and there is no cross-page write primitive. The graph projection is page-scoped: every page write rewrites the nodes of the Subjects that page holds and tears down the ones it no longer does, so the page written last is the one that decides what the moved Subject's node looks like. Writing the source last strips the node the target write has just built and leaves a bare `:Subject` stub belonging to no page until the next rebuild, while the wiki database and the index both look correct. The cost of source-first is a window in which the Subject is on no page; a target write that fails is answered by writing the source page back exactly as it was read. **Move is offered on the main-subject row as well**, with a warning that the page will be left without a main subject. Withholding it would only have hidden the action behind a demote-then-move dance. **The promotion checkbox is never checked for you**, and once a target is chosen it names the Subject that promoting would demote, so an effect on a page the user is not looking at is stated rather than sprung. **Page search is MediaWiki's own** `/v1/search/title`, which already carries page ids and drops results the viewer may not read. It is scoped to the wiki's default search namespaces, so a page outside them has to be typed in full. NeoWiki adds no page-search endpoint of its own. **The target page is not restricted by namespace**, because creating a Subject is not either. Restricting only move would be inconsistent. ## Manual Browser Check On a wiki with the demo data, as a user who can edit: 1. Open `Rijksmuseum`, then the **Data** tab. Every row has a Move button between copy-link and delete; narrow the window past the mobile breakpoint and it is in the ⋯ menu instead, above Delete. 2. Move one of the *Rijksmuseum attendance* subjects. The dialog opens with Move disabled. Type `ACME` — matching pages appear, with **Use "ACME" as a new page** last, under a separator. 3. Pick `ACME Inc`. Tick the checkbox: it names *ACME Inc*'s current main subject as the one that would be demoted. Untick it and move. A green toast names the subject and its new page, and the row leaves the listing. 4. Open `ACME Inc`'s Data tab: the attendance subject is there as a child. Its ID is unchanged. 5. Move it back to `Rijksmuseum`. On the source row for the **main** subject, open Move: an inline warning says the page will be left without a main subject. 6. In the dialog, type a title no page has and pick **Use "…" as a new page**, then Move. The page is created and the subject lands on it; visit the page to confirm. 7. Type a title, pick the create option, then correct the text without re-picking. Move is offered only once a target is chosen again — it must not create the page you first typed. ## Testing 1903 vitest and 1979 PHPUnit tests pass, plus the `@group Database` endpoint tests. The new tests were each mutation-verified: every guard inverted and seen to fail by name. One of them mounts the real graph and asserts the moved Subject has exactly one hosting page node and keeps its schema label, which is what the source-first ordering buys. Exercised end to end against a dev wiki: a Subject moved between pages and back, its relations still resolving, both pages carrying the edit summary, and every refusal path (400, 403, 404, 409, missing CSRF) answered as documented. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012KQMvby9ED7qd7E2XC6csE
PageSubjects::without() answers a copy without the given Subject. The move writes that copy and the rollback writes back the object it read, which nothing has mutated. The second read of the source page goes, and with it the reliance on repository reads handing out distinct objects, which SubjectRepository never promised. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A source page that vanishes between the read check and its write is written first, so nothing has been written yet; from the caller's side the Subject is gone. presentSourcePageNotFound() goes. The response is still 404, with the message "Subject not found" instead of "Page not found". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The production HTTP client rejects on every non-2xx status but 422, so a non-ok Response never carried a server message. moveSubject now fails generically on one, like every sibling method; the rejection path still delivers the server's own message to the dialog, unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
MediaWikiPageCreator wraps mw.Api().create behind the service registry. MoveSubjectDialog consumes its outcome and reads the Subject repository through NeoWikiServices instead of the NeoWikiExtension singleton, so the dialog no longer knows MediaWiki error codes and its spec provides both collaborators. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Source-page 403, source untouched on an unreadable target, and the edit summary reaching both pages are each asserted in MoveSubjectActionTest; the target-page 403 test keeps proving the Authority wiring through the real stack. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Member
Author
|
Ugh. Try to get claude to remove lines. 🤔 |
JeroenDeDauw
force-pushed
the
feature/1338-move-subject-to-page
branch
from
September 6, 2026 22:50
5c41fec to
583af10
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follows-up to #1356, into its branch. One commit per item, so any of them can be dropped on its own. User-visible behaviour is unchanged; the one API-visible difference is under the second item.
PageSubjects::without()answers a copy without the Subject. The move writes the copy, and the rollback writes back the object it read, which nothing has mutated. The second read of the source page goes, and with it the reliance on repository reads handing out distinct objects, whichSubjectRepositorynever promised.presentSourcePageNotFoundgoes. The response is still 404, with the messageSubject not foundinstead ofPage not found.moveSubjectno longer reads a server message off a non-ok Response. The production client rejects on every non-2xx but 422, so that branch never carried one. The rejection path still delivers the server's message to the dialog, unchanged.MediaWikiPageCreatorwrapsmw.Api().create;MoveSubjectDialogconsumes its outcome and reads the Subject repository throughNeoWikiServicesinstead of theNeoWikiExtensionsingleton. The dialog no longer knows MediaWiki error codes.@group Databasetests dropped: source-page 403, source untouched on an unreadable target, edit summary on both pages.MoveSubjectActionTestcovers each; the target-page 403 test keeps proving the Authority wiring.Considered, omitted:
dropFromRegistryOnceUnlistedwith a plain listing reload: the moved Subject's registry copy carries its old page, so dropping it does real work.PagePickerandSubjectPicker: the largest duplication in the PR, but it means reworking SubjectPicker; a PR of its own.Production notes
Design, diff review and this text by
Fable 5.1 (max); implementation and tests by anOpus 5 (max)subagent from a written spec. The dev-wiki check covered the page-creation path only; the other paths rest on the retained unit and Database-group tests.