feat(admin): an empty dashboard says what to do next - #1881
mobeenabdullah wants to merge 10 commits into
Conversation
… the grid LOCAL CHECKPOINT on task:an-empty-dashboard-says-what-to-do-next. Not pushed, and not shippable on its own: the seed offer is no longer drawn anywhere until the empty dashboard that hosts it lands in the next commits. Done here: - `core/seed-demo-content` removed from CORE_WIDGETS, and the checklist's ordering comment that referred to it rewritten. - `core#SeedDemoContentCard` unregistered from core-components, with its runtime registry test. - core-widgets.test.ts: the seed-specific test removed and the conditional membership list updated. - lifecycle.ts: the module docblock's example no longer names the seed card. - `DashboardLayoutResponse.contentEmpty?: boolean` declared in the admin. Remaining, per HANDOFF-2026-09-13-widgets-8.md section 3.
…rd-says-what-to-do-next
A reader who can see no content is shown one next step in place of the cards: create a collection, take the template's demo content, add a first entry, or a plain note for a reader who can take none of those steps. The layout read reports contentEmpty from the same condition evaluation that filters the placements, and never folds it into the scope token. The admin draws EmptyDashboard, which owns the seed state, holds itself through a seed until the reader continues, announces the seed through the grid's live region, and leaves card data unrequested while it stands in for the cards. The seed offer is no longer a grid card.
…rd-says-what-to-do-next
…actions The decision to draw the empty dashboard, the hold that keeps it through a seed, and the list of cards the batch asks for now live in one hook beside askedWith, so the grid cannot ask for cards the empty state is hiding. The grid passes ArrangedColumns a showEmpty flag with the empty state, and the per-card actions as one `on` object, the grouping ArrangedCell already takes.
|
Warning Review limit reachedNext included review available in 22 minutes. View limit detailsLimit details: You’ve used the included review currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Advanced Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (24)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d3e0499f5c
ℹ️ 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".
| const { showEmpty, hold, cards } = useEmptyDashboardGate( | ||
| layout.layout?.contentEmpty === true, | ||
| editor.isEditing, | ||
| visible |
There was a problem hiding this comment.
Count Singles before replacing the dashboard
When a reader has a populated Single but no collection documents, readerHasContent still returns false because it only iterates collection slugs (reader-content.ts:190-203). This new gate therefore replaces the entire grid, including the core/singles card and its quick links, with the empty-dashboard message even though reader-visible content exists. Include readable Single documents in the content verdict, or avoid suppressing their card based solely on collection-row emptiness.
Useful? React with 👍 / 👎.
…rd-says-what-to-do-next
… twice The seed probe read a 401 or 403 from the seed route as "the endpoint exists", while the route answers it with the super-admin check its POST enforces. A reader the route refuses was offered a button whose every press fails, drawn in place of the entry step they could take. The probe now offers the seed only on a 2xx. A seed run is now recorded under a mutation key. useSeedStatus reports "seeding" while any run is in flight, wherever it started, and startSeed refuses a second one. The empty dashboard unmounts in edit mode, and a seed started there and returned to after cancelling read as never started.
…rd-says-what-to-do-next
@nextlyhq/adapter-drizzle
@nextlyhq/adapter-mysql
@nextlyhq/adapter-postgres
@nextlyhq/adapter-sqlite
@nextlyhq/admin
@nextlyhq/admin-css
@nextlyhq/blocks-engine
@nextlyhq/blocks-react
@nextlyhq/builder
create-nextly-app
@nextlyhq/eslint-plugin
nextly
@nextlyhq/plugin-form-builder
@nextlyhq/plugin-mcp
@nextlyhq/plugin-page-builder
@nextlyhq/plugin-sdk
@nextlyhq/plugin-seo
@nextlyhq/storage-s3
@nextlyhq/storage-uploadthing
@nextlyhq/storage-vercel-blob
@nextlyhq/ui
commit: |
…rd-says-what-to-do-next
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b3923f500f
ℹ️ 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".
| const status: SeedStatus = (() => { | ||
| if (overlay) return overlay; | ||
| if (seedsInFlight > 0) return { kind: "seeding" }; |
There was a problem hiding this comment.
Preserve completed seed outcomes across remounts
When a seed starts and edit mode unmounts EmptyDashboard, cancelling mounts a new useSeedStatus instance that can observe only seedsInFlight. When the request completes, its result or error is written to the old instance's local overlay; this new instance instead falls through to the status queries, so a success jumps to the grid without its summary and a failure silently becomes the idle seed offer again. Fresh evidence beyond the earlier race report is that the fix transfers only the pending count, not mutation data or errors; the new edit-mode test also stops immediately after settlement rather than observing the outcome. Preserve the settled mutation state in shared storage too.
AGENTS.md reference: AGENTS.md:L228-L233
Useful? React with 👍 / 👎.
| {/* Only where declining is still a choice. Mid-seed there is nothing | ||
| to decline, and after a success Continue is the way on -- a skip | ||
| there would record the offer as declined after it was accepted. */} | ||
| {(status.kind === "idle" || status.kind === "error") && ( |
There was a problem hiding this comment.
Persist success when the completion marker fails
When the blog seed succeeds but writing seed.completedAt fails, the route deliberately returns success with a warning (templates/blog/src/app/admin/api/seed/route.ts:116-125). This success state now hides both skip controls and offers only Continue, which merely releases local UI state, so neither completion nor skip is persisted. After a remount the offer is considered idle again, suppressing WelcomeHeader and allowing the seed prompt to return if the content is later emptied. Provide a way to retry or otherwise persist the resolved offer before releasing it.
Useful? React with 👍 / 👎.
…rd-says-what-to-do-next
|
Merged current Why the run was redStep-level conclusions on the failing job show exactly one failing step — step 13, "Script tests" (
Merging Verification on the merged treeFull local run of this job's steps against current
Pre-push hook bypassed, deliberatelyThis push used
Established rather than assumed:
The hook's own run on this commit reported 12274 passed, 1 failed — that one failure being this test. Every other gate the hook covers was run and is listed above. Worth noting separately: that test is environment-fragile rather than wrong, since it depends on real filesystem permissions instead of a mocked The three open review findings on this PR are not addressed by this push and are still outstanding. @codex please review this PR Generated by Claude Code |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 580d36b10e
ℹ️ 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".
| if (onboarding.isPending) return { kind: "pending" }; | ||
| if (onboarding.isUnavailable) return { kind: "unavailable" }; |
There was a problem hiding this comment.
Prioritize active seed state over onboarding errors
When EmptyDashboard remounts during a seed—for example after cancelling dashboard editing—the stale onboarding query refetches; if that refetch fails, this branch replaces the live seeding, error, or warning-bearing success body with the actionless unavailable message. The seed's holding flag nevertheless remains true for errors and warning outcomes, so Retry, Skip, and Continue are all inaccessible and the page remains trapped until an unrelated onboarding refetch succeeds. Preserve active seed states ahead of onboarding loading/error states.
Useful? React with 👍 / 👎.
| const release = useCallback(() => { | ||
| leaving(); | ||
| setReleased(true); | ||
| }, [leaving]); |
There was a problem hiding this comment.
Let Continue override a stale empty-content verdict
If the layout invalidation triggered after a successful seed is slow or fails, clicking Continue only sets released; useEmptyDashboardGate still sees the cached contentEmpty: true and keeps this component mounted. The success card then disappears in favor of the stale “Add your first entry” body, so the explicit Continue action does not reach the dashboard until a later layout refetch succeeds. Releasing a completed seed should suppress the old empty verdict or wait for a confirmed replacement before offering Continue.
Useful? React with 👍 / 👎.
What a reader sees
A reader who can see no content yet no longer meets a dashboard of empty card frames. The page says one thing to do next, chosen from what the server says this reader can do:
While the setup steps load it shows a placeholder; if they cannot be read it says so and offers nothing. Edit dashboard always shows the ordinary grid, so the arrangement is never trapped behind the message. The state is detected on every read and never remembered: the first entry written anywhere in the admin replaces it with the cards.
How
Core (
nextly)layoutConditions(widgets, caller)replaceswidgetsWhoseConditionHolds(its only callers were the layout GET and PUT). It evaluates the conditions widgets declare pluscontent:empty, in one evaluation, and returns{ widgets, contentEmpty }— so the empty state and any card naming that condition cannot disagree. The count is memoised by the per-request probe and short-circuits for a reader who may read no collection.contentEmptyistrueonly on a verdict: a count that failed is not an empty install./api/dashboard/layoutreturnscontentEmptybesideplacements/available; PUT echoes it. It is not folded into thescopetoken — content appearing must not turn a reader's next save into a 409.core/seed-demo-contentis gone from the core widgets, and its component registration from the admin.Admin
EmptyDashboard(new) ownsuseSeedStatus()anduseOnboardingSteps()and picks the body. It owns the seed state because the instance that runs the seed has to be the one choosing the body — the mid-flight state lives in that instance.SeedDemoContentCardis presentational now (status,onSeed,onSkip,onContinue). After a success it offers Continue to your dashboard. The ✕ skip control is revealed on keyboard focus as well as on hover, and is not offered after a success, where it would have recorded the offer as declined after it was accepted.Decisions taken while building
ListEmptyState(components/ui/table/list-view) for the empty bodies instead of extracting a new shell fromEmptyEntitiesState. It already exists as the one component for icon + heading + explanation + action; a second shell would have been a parallel implementation.EmptyEntitiesStateis unchanged.WidgetGrid, notArrangedColumns: the same always-mounted property one level up, so the grid can also stop requesting card data while the empty state is shown. Otherwise it spends a round trip on cards nobody sees and announces "N of M widgets updated" over a page that shows none of them. One hook,useEmptyDashboardGate, owns the decision, the hold and the list of cards the batch asks for, so the three cannot disagree about what is on screen.ArrangedColumnsdraws the empty state inside its own section (showEmpty+emptyState), so the live region and the focus target stay mounted across the swap. Its six per-card handler props are now oneonobject — the groupingArrangedCellalready takes — which keeps the component inside the cognitive-complexity budget it was sitting exactly at.Verification
Tests
conditions.test.ts+widget-layout.test.tspass (90). SQLite integrationconditions-content.integration.test.tspasses (5), including a new case where a row the reader may not read leavescontentEmptytrue and a row they may read moves it.EmptyDashboard.test.tsx(new, 16) covers body selection for each reader, the placeholder and unreadable states, all four seed announcements, the hold from the moment a seed starts, Continue, the five-second release (and no release while warnings wait), focus moving only when it was inside, and skipping a failed seed.WidgetGrid.edit.test.tsxgains 6 grid-level cases: the empty state in place of the cards, the cards for a server that does not sendcontentEmpty, the cards while editing, no card request kept alive behind the empty state, a seed's outcome held after the host says there is content, and seed progress through the grid's one live region. That last pair also pins thatinvalidateQueries({ queryKey: ["dashboard"] })— what the entry create/update/delete hooks send — reaches the layout read by prefix.Break-verified by name — each wrong implementation written back, the named test confirmed failing, the file restored byte-identical, and the unbroken suites green before and after:
content:emptynot asked for the dashboard · an absent verdict read as empty · the dashboard's answer from a second evaluation · GET omits the field · the field folded intoscope· PUT omits the echo · PUT echoes a second evaluation. Integration, 1 of 1:content:emptynot asked.dashboardprefix · the empty state announcing nowhere ·announceStatussaying nothing · the columns ignoring the empty state. Two tests first survived their breaks (a placeholder assertion that ran before the offer settled; a focus assertion satisfied by an earlier click) and were repaired until they failed.showEmpty· the grouped actions crossed (removeandtoggleHiddenswapped) · the layout key losing itsdashboardprefix · the empty state announcing nowhere ·announceStatussaying nothing.Gates
pnpm check-typespass ·pnpm lintpass.pnpm check-typespass (both programs) ·pnpm lintpass · unit suite 12,260 passed, 1 failed, 42 skipped. The failure is a 60 sbeforeEachtimeout inroute-handler/set-handler-plugins.test.ts, whose hook re-importsauth-handleraftervi.resetModules()(the file's own docblock records that re-import pulling the whole graph). In the full 956-file run that file took 94.5 s; run alone it passes 13/13 in 34.8 s. This branch's only import change in nextly source swapswidgetsWhoseConditionHoldsforlayoutConditionsfrom the same module, so it adds nothing to that graph.fallow audit(new-only) at the pushed head: pass — 23 changed files, 0 introduced findings. The first run failed on cognitive complexity inWidgetGrid(19) andArrangedColumns(17); the refactor above brought both back within the budget of 15.Follow-ups filed, not in this PR
finding:welcome-header-drops-the-only-h1-while-the-seed-offer-is-unanswered— pre-existing: the welcome header, the page's only<h1>, hides whenever the seed offer is unanswered, even on a dashboard with content and no offer on screen.finding:the-seed-probe-offers-seeding-to-a-reader-the-route-refuses— pre-existing: the probe reads a 401/403 from the seed route as available.finding:an-empty-install-paints-its-cards-before-the-empty-dashboard— the card frames render until the layout read lands withcontentEmpty.