feat(rm-handlers): home overview + master layout + create forms (project/news/wiki) & CTAs#24
Conversation
The two deferred POC-polish items that make the demo navigable end-to-end without hand-typing URLs. - home: new cross-resource overview at `/` (rm_handlers::home). Lists every seeded concept (Issues, Projects, Time entries, News, Wiki, Users, Roles, Repositories, Issue statuses, Trackers, Priorities, Custom queries, Relations) with its live row count and a link to the resource's list route, plus a "New issue" quick action. Pure render_home() is split from the handler so the markup is unit-tested without booting a store; counts come from each Store::list_*().len() (becomes COUNT(*) when a SQL backend lands — handler shape unchanged). - list_chrome: generic render_action_bar(&[(label, href)]) — Redmine's top-right `.contextual` action strip. HTML-escapes label + href; an empty slice yields no stray <nav>. W2/W3/detail pages reuse it. - issues: wire the action bar into the list with a "New issue" link to the D1 create form (/issues/new). - rm-server: mount home::router at `/` in build_router_with, replacing the storeless proof-of-shape index (which stays on the scaffolding build_router()). CI gate green locally on toolchain 1.95: cargo fmt --all --check, clippy --all-targets --workspace -D warnings, test --workspace (6 new tests, all green).
wrap_in_doc now emits the master-layout skeleton every page shares: - #top-menu — persistent global nav (Home, Projects, Issues, Spent time, News, Wiki) as a const, spliced verbatim (no per-request alloc beyond the outer format!). - #header — app title linking home. - #main > #content — the fragment wrapper. - viewport meta for the eventual responsive stylesheet. The id/class hooks mirror Redmine's own markup so G1's base.askama template + stylesheet drop in cosmetically over this shell. Title stays HTML-escaped (XSS guard, codex P1 on PR #10). All existing page-body assertions are substring checks, so the added chrome doesn't break them. CI gate green on toolchain 1.95: fmt --all --check, clippy --all-targets --workspace -D warnings, test --workspace.
D1 create flow for Project, mirroring issues_form one resource over. - projects_form: GET /projects/new + POST /projects. Name (required, <=255) + Identifier (required, <=100, Redmine slug rule: starts with a lowercase letter, then [a-z0-9_-]). 303 to /projects/<identifier> on success (POST-redirect-GET); 200 re-render with inline errors on failure, input preserved. Pure validate() + is_valid_identifier() are fully unit-tested. - projects: "+ New project" contextual action on the list -> /projects/new. - rm-server: merge projects_form::router. The static /projects/new coexists with /projects/:identifier — matchit gives the static segment priority, same proven shape as /issues/new vs /issues/:id. CI gate green on toolchain 1.95: fmt --all --check, clippy --all-targets --workspace -D warnings, test --workspace.
News is the third form module, so per the codebase's "factor on the third caller" rule (Plan §1.6) the duplicated form-error block lifts into common::render_errors (now pub(crate)); issues_form + projects_form drop their copies and import it. - news_form: GET /news/new + POST /news. Title (required, <=255) + Summary (optional, <=255) + Description (required), mirroring Redmine's News validations. 303 to /news/<id> on success (record-keyed like Issue); 200 re-render with inline errors on failure. Pure validate() unit-tested. - news: "Add news" contextual action on the list -> /news/new. - common: render_errors moved here + directly unit-tested. - rm-server: merge news_form::router. CI gate green on toolchain 1.95: fmt --all --check, clippy --all-targets --workspace -D warnings, test --workspace.
Fourth create form, completing the "create any core resource" set (Issue, Project, News, Wiki) on the shared form chrome. - wiki_form: GET /wiki/new + POST /wiki. Title (required, <=255 — it's the URL slug) + Body (optional — a stub page is valid). 303 to /wiki/<title> on success (title URL-encoded via encode_path_segment, so spaces work); 200 re-render with inline errors on failure. - wiki_pages: "New wiki page" contextual action on the list -> /wiki/new. - rm-server: merge wiki_form::router. Documented MVP routing caveat: /wiki/new shadows a page literally titled "new" (static beats /wiki/:title); nested /projects/:id/wiki/new lands with the project sidebar follow-up. CI gate green on toolchain 1.95: fmt --all --check, clippy --all-targets --workspace -D warnings, test --workspace.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cd2b270cef
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| name: form.name.trim().to_string(), | ||
| identifier: form.identifier.trim().to_string(), | ||
| }; | ||
| let project = state.store.create_project(new).await?; |
There was a problem hiding this comment.
Reject duplicate project identifiers before creating
When a user submits an identifier that already exists, this path still inserts a new project because Store::create_project currently just issues CREATE project and its docs note uniqueness conflicts only once an index lands; the detail route then looks up by identifier with LIMIT 1, so the 303 to /projects/<identifier> can show the older project and both rows share the same URL. Please check for an existing identifier and re-render the form with a validation error before calling create.
Useful? React with 👍 / 👎.
| title: form.title.trim().to_string(), | ||
| body: form.body.trim().to_string(), | ||
| }; | ||
| let page = state.store.create_wiki_page(new).await?; |
There was a problem hiding this comment.
Reject duplicate wiki titles before redirecting
When someone submits a wiki title that already exists, this creates a second wiki_page row even though the title is the URL key; the list links both rows to the same /wiki/<title> URL and the detail lookup uses the title with LIMIT 1, so the redirect can land on the existing page instead of the one just created. Please validate that the title is unused and show an inline error before inserting.
Useful? React with 👍 / 👎.
Summary
POC-polish pass that makes the demo navigable end-to-end and rounds out the "New X" create forms across the seeded resources. Five commits on top of
main(56da8d1), touching onlyrm-handlersandrm-server.Commits
/issues— cross-resource overview at/(rm_handlers::home) listing every seeded concept (Issues, Projects, Time entries, News, Wiki, Users, Roles, Repositories, Issue statuses, Trackers, Priorities, Custom queries, Relations) with live row counts and links to each list route, plus a "New issue" quick action. Purerender_home()split from the handler for unit testing; counts come from eachStore::list_*().len(). Adds genericrender_action_bar(&[(label, href)])(Redmine's.contextualstrip, HTML-escaped) reused by W2/W3/detail pages.render_errorstocommon.Changes
rm-server:home::routeris mounted at/inbuild_router_with, replacing the storeless proof-of-shape index (which stays on the scaffoldingbuild_router()).Notes
list_*().len()today and becomeCOUNT(*)when a SQL backend lands — handler shape unchanged.cargo fmt --all --check,clippy --all-targets --workspace -D warnings,test --workspace), with new unit tests for the added render functions.🤖 Generated with Claude Code
Generated by Claude Code