Skip to content

Fix RecordTable overflow; expand platform integrations editor - #1354

Merged
kentcdodds merged 2 commits into
mainfrom
cursor/packages-table-scroll-platform-integrations-7faa
Aug 10, 2026
Merged

kentcdodds merged 2 commits into
mainfrom
cursor/packages-table-scroll-platform-integrations-7faa

Conversation

@kentcdodds

@kentcdodds kentcdodds commented Aug 10, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • Fixed RecordTable clipping wide rows by giving the table a horizontal scroller (overflow-x: auto), clipping the card with overflow: clip, and clamping primary cell names so package lists stay readable.
  • Migrated /admin/platform-integrations onto the same RecordTable with mode="expand", so selecting a row unfolds the create/edit form under that row (create uses /new as an orphaned pane below the table).
  • Wired list/detail/new routes for platform integrations and documented the editor-in-expand exception in the 0010 page inventory.

Test plan

  • RecordTable unit tests (contracts + overflow CSS)
  • Admin platform integrations handler tests
  • Full unit suite + e2e via push hooks
  • Spot-check /account/packages no longer clips Tags/Updated
  • Spot-check /admin/platform-integrations expand β†’ edit form, Create β†’ /new
System recap β€” composes existing primitives (low risk)

Mode: recap Β· Base: main @ 28b74f25 Β· Head: 5c948a06

Classification: composes β€” shared RecordTable overflow fix and admin UI migration onto existing list/detail routing; no new backend primitives.

Primitives touched

Primitive Group Impact
app-ui surfaces composes β€” RecordTable scroll + platform-integrations expand/edit

System map

Admin platform integrations and account packages both render through the shared RecordTable; detail selection stays URL-driven.

Legend: green = composes (wiring only) Β· amber = extended by this PR Β· red = new primitive Β· gray = context (unchanged, included only when an edge crosses it).

flowchart LR
	appUi["app-ui<br/>Browser app (Remix 3)"]:::touched
	appUi -->|"RecordTable overflow-x + expand form"| appUi
	classDef touched fill:#1a7f37,color:#fff
	classDef extended fill:#9a6700,color:#fff
	classDef added fill:#cf222e,color:#fff
	classDef untouched fill:#57606a,color:#fff
Loading

Before / after

Surface Before After
RecordTable Wide rows clipped by overflow: hidden Horizontal scroller + primary cell clamp
/admin/platform-integrations Card stack + bottom create/edit panel RecordTable expand with form under the row
Open in WebΒ Open in CursorΒ 

Summary by CodeRabbit

  • New Features

    • Added searchable list and detail views for admin platform integrations.
    • Added dedicated routes for creating and editing integrations.
    • Added controls to enable, disable, delete, and manage integration logos.
  • Improvements

    • Wide tables now scroll horizontally instead of clipping content.
    • Package names and identifiers are better truncated for compact displays.
  • Documentation

    • Updated screen inventory and table guidance for integration editors and horizontal scrolling.

Replace the card list and bottom panel with RecordTable mode=expand, where
the expanded row (or orphaned pane for /new) renders the create/edit form.
Use createListDetailRoute for selection URLs and replaceLocation after
create, delete, and cancel. Document the editor-in-expand exception in the
0010 page inventory.
Give the shared record table a horizontal scroller (and clamp primary
cells) so wide package rows no longer clip under the card edge, and
register list/detail/new routes so admin platform integrations can
expand into the edit form.
@coderabbitai

coderabbitai Bot commented Aug 10, 2026 •

Copy link
Copy Markdown

Review Change Stack

πŸ“ Walkthrough

Walkthrough

Admin platform integrations now use URL-based list, create, and detail routes. The page uses a searchable RecordTable with an inline editor. RecordTable now supports horizontal scrolling and improved cell sizing. Related route wiring, preload registration, package-table styling, tests, and documentation were updated.

Changes

Platform integrations and RecordTable

Layer / File(s) Summary
Platform integration route wiring
packages/worker/universal/routes.ts, packages/worker/src/app/router.ts, packages/worker/client/routes/index.tsx, packages/worker/client/lazy-route.tsx
New and detail platform integration routes are defined, connected to the existing handler and client route component, and registered for preload.
RecordTable overflow and cell layout
packages/worker/client/routes/record-table.tsx, packages/worker/client/routes/record-table.node.test.ts
RecordTable now uses horizontal scrolling, preserves a minimum table width, constrains primary cells, supports expandable editors, and tests the overflow styles.
Routed integration list and editor
packages/worker/client/routes/admin-platform-integrations.tsx
The page derives selection and search from the URL. It renders a searchable table and supports create, edit, enable, delete, cancel, and logo actions.
Related inventory and package table alignment
docs/contributing/decisions/0010-account-record-table/page-inventory.md, packages/worker/client/routes/account-packages.tsx
The inventory documents the platform integrations editor exception. Package names and Kody IDs use clamped cells, and the Updated column has explicit drop priority.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant AdminPlatformIntegrationsRoute
  participant RecordTable
  participant PlatformIntegrationsHandler

  Browser->>AdminPlatformIntegrationsRoute: open create or detail route
  AdminPlatformIntegrationsRoute->>RecordTable: render filtered integrations
  Browser->>AdminPlatformIntegrationsRoute: submit integration form
  AdminPlatformIntegrationsRoute->>PlatformIntegrationsHandler: send create or update action
  PlatformIntegrationsHandler-->>AdminPlatformIntegrationsRoute: return saved integration
  AdminPlatformIntegrationsRoute-->>Browser: navigate to saved integration detail
Loading

Possibly related PRs

Suggested reviewers: vojtaholik

πŸš₯ Pre-merge checks | βœ… 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
βœ… Passed checks (4 passed)
Check name Status Explanation
Title check βœ… Passed The title clearly summarizes the two main changes: fixing RecordTable overflow and expanding the platform integrations editor.
Description check βœ… Passed The description covers the summary, testing, and system changes, but it omits the required Intent section.
Linked Issues check βœ… Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check βœ… Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches πŸ’‘ 1
πŸ“ Generate docstrings πŸ’‘
  • Create stacked PR
  • Commit on current branch
πŸ§ͺ Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/packages-table-scroll-platform-integrations-7faa

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.

❀️ Share

Comment @coderabbitai help to get the list of available commands.

@kentcdodds
kentcdodds marked this pull request as ready for review August 10, 2026 02:47
@github-actions

Copy link
Copy Markdown
Contributor

πŸ”Ž Preview deployed: https://kody-pr-1354.kody-a99.workers.dev

Worker: kody-pr-1354
D1: kody-pr-1354-db
KV: kody-pr-1354-oauth-kv

Mocks:

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 5c948a0. Configure here.

if (!routeData && needsLoad && typeof document !== 'undefined') {
status = 'loading'
loadingForHref = currentHref
handle.queueTask(loadPlatformIntegrations)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Search keystrokes refetch list

Medium Severity

Integration search filters with filterApps on the loaded apps list, but each RecordTableSearch update rewrites the URL and needsLoad treats any currentHref !== lastLoadedHref as a full reload. That sets status to loading, hides the table, and POSTs to the JSON API on every keystroke even though the request ignores q.

Additional Locations (1)
Fix in CursorΒ Fix in Web

Reviewed by Cursor Bugbot for commit 5c948a0. Configure here.


function handleEdit(app: AdminPlatformIntegrationApp) {
editingApp = app
pendingLogoBase64 = undefined

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Edit form stale after save

Medium Severity

After a successful edit save the route stays on the detail URL and calls form.reset() while the form key remains edit-${slug}. Uncontrolled fields keep their first-mount defaultValue/defaultChecked, so the UI can show pre-save values even though applyData refreshed apps from the server.

Additional Locations (1)
Fix in CursorΒ Fix in Web

Reviewed by Cursor Bugbot for commit 5c948a0. Configure here.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
packages/worker/client/routes/admin-platform-integrations.tsx (1)

820-836: πŸ“ Maintainability & Code Quality | πŸ”΅ Trivial | πŸ’€ Low value

Collapse the duplicated cancel button.

Both branches render the same button with the same handler, the same disabled condition, and the same style. Only the label differs.

♻️ Proposed refactor
-						{isEditing ? (
-							<button
-								type="button"
-								disabled={actionState !== 'idle'}
-								mix={[on('click', cancelEditor), css(secondaryButtonCss)]}
-							>
-								Cancel edit
-							</button>
-						) : (
-							<button
-								type="button"
-								disabled={actionState !== 'idle'}
-								mix={[on('click', cancelEditor), css(secondaryButtonCss)]}
-							>
-								Cancel
-							</button>
-						)}
+						<button
+							type="button"
+							disabled={actionState !== 'idle'}
+							mix={[on('click', cancelEditor), css(secondaryButtonCss)]}
+						>
+							{isEditing ? 'Cancel edit' : 'Cancel'}
+						</button>
πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/client/routes/admin-platform-integrations.tsx` around lines
820 - 836, Collapse the duplicated button in the isEditing conditional into a
single shared button, retaining its type, disabled condition, click handler, and
styling while conditionally rendering only the label as β€œCancel edit” or
β€œCancel”.
packages/worker/client/routes/record-table.node.test.ts (1)

164-177: πŸ“ Maintainability & Code Quality | πŸ”΅ Trivial | ⚑ Quick win

Add the negative half of the expand contract.

The test asserts the scroller styles, which now exist for every mode. It does not lock the behavior the change actually introduced for expand: the scroller must stay uncapped, while pane and none keep the height cap. A regression that caps expand would still pass this test.

πŸ§ͺ Suggested extra assertions
 	// Remix serializes longhands with a space after the colon.
 	expect(html).toContain('overflow-x: auto')
 	expect(html).toContain('overflow: clip')
+	// `expand` renders the record inside the scroller, so it must not cap height.
+	expect(html).not.toContain('max-height')
+
+	const cappedHtml = await renderToString(
+		jsx(RecordTable, { mode: 'pane', ariaLabel: 'Packages', columns, rows }),
+	)
+	expect(cappedHtml).toContain('max-height')
+	expect(cappedHtml).toContain('overflow-y: auto')
 })
πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/client/routes/record-table.node.test.ts` around lines 164 -
177, Strengthen the RecordTable expand-mode test around RecordTable so it
verifies the expand scroller remains uncapped, while pane and none retain the
height cap. Add assertions that distinguish the mode-specific height styles,
preserving the existing horizontal-overflow checks.
packages/worker/client/routes/record-table.tsx (1)

267-273: πŸ“ Maintainability & Code Quality | πŸ”΅ Trivial | πŸ’€ Low value

Use an in-cell clamp instead of maxWidth on this <td>.

primaryCellCss is applied to a table cell, where max-width is not reliably respected in auto table layout and cannot ensure the primary name does not force the table width. Apply recordCellClamp(...) to block content inside the primary cell, or use another established inner-content clamp, otherwise this cap provides little sizing constraint.

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/client/routes/record-table.tsx` around lines 267 - 273,
Replace the maxWidth-based sizing in primaryCellCss with the established
recordCellClamp(...) applied to block content inside the primary cell. Preserve
the existing responsive whiteSpace behavior while ensuring the primary name is
constrained within the cell rather than relying on table-cell max-width.
πŸ€– Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/worker/client/routes/admin-platform-integrations.tsx`:
- Around line 857-869: Key platform-integrations loading state to the
payload-relevant dataHref rather than the full currentHref, so query updates and
list/detail navigation do not refetch or unmount the table. Update the needsLoad
comparisons and the corresponding assignments inside loadPlatformIntegrations,
including the nearby render logic, while preserving normal loading for genuinely
different payloads.
- Around line 434-445: Preserve the active search query across integration
navigation by passing the current search string to the route builders in the
post-create navigation, handleDelete, cancelEditor, and startCreateIntegration.
Reuse the same search value already supplied by the row links with
buildListHref() and buildNewHref(), so q remains intact when leaving or creating
an integration.

---

Nitpick comments:
In `@packages/worker/client/routes/admin-platform-integrations.tsx`:
- Around line 820-836: Collapse the duplicated button in the isEditing
conditional into a single shared button, retaining its type, disabled condition,
click handler, and styling while conditionally rendering only the label as
β€œCancel edit” or β€œCancel”.

In `@packages/worker/client/routes/record-table.node.test.ts`:
- Around line 164-177: Strengthen the RecordTable expand-mode test around
RecordTable so it verifies the expand scroller remains uncapped, while pane and
none retain the height cap. Add assertions that distinguish the mode-specific
height styles, preserving the existing horizontal-overflow checks.

In `@packages/worker/client/routes/record-table.tsx`:
- Around line 267-273: Replace the maxWidth-based sizing in primaryCellCss with
the established recordCellClamp(...) applied to block content inside the primary
cell. Preserve the existing responsive whiteSpace behavior while ensuring the
primary name is constrained within the cell rather than relying on table-cell
max-width.
πŸͺ„ Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
βš™οΈ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a68f3f70-5573-4352-9894-42b0d9b259f9

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 28b74f2 and 5c948a0.

πŸ“’ Files selected for processing (9)
  • docs/contributing/decisions/0010-account-record-table/page-inventory.md
  • packages/worker/client/lazy-route.tsx
  • packages/worker/client/routes/account-packages.tsx
  • packages/worker/client/routes/admin-platform-integrations.tsx
  • packages/worker/client/routes/index.tsx
  • packages/worker/client/routes/record-table.node.test.ts
  • packages/worker/client/routes/record-table.tsx
  • packages/worker/src/app/router.ts
  • packages/worker/universal/routes.ts

Comment on lines +434 to +445
function cancelEditor() {
resetSelectionState()
replaceLocation(platformIntegrationsRoute.buildListHref())
handle.update()
}

const primaryButtonCss = getPillButtonCss({ size: 'sm' })
const secondaryButtonCss = getGhostButtonCss({ size: 'sm' })
const dangerButtonCss = getDangerPillCss({ size: 'sm' })
function startCreateIntegration() {
if (actionState !== 'idle') return
resetSelectionState()
replaceLocation(platformIntegrationsRoute.buildNewHref())
handle.update()
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟑 Minor | ⚑ Quick win

Cancel, create, and post-create navigation discard the active search.

cancelEditor calls buildListHref() and startCreateIntegration calls buildNewHref(), both with no argument, so the q parameter is dropped. handleDelete at Line 428 and the post-create navigation at Line 392 do the same.

The row links at Lines 958-963 do pass the current search through, so the behavior is inconsistent: opening a record keeps the filter, and leaving it clears the filter. createListDetailRoute accepts a search string on all three builders.

πŸ”§ Proposed fix
 	function cancelEditor() {
+		const search = new URL(readCurrentRouterHref(handle), 'http://localhost')
+			.search
 		resetSelectionState()
-		replaceLocation(platformIntegrationsRoute.buildListHref())
+		replaceLocation(platformIntegrationsRoute.buildListHref(search))
 		handle.update()
 	}
 
 	function startCreateIntegration() {
 		if (actionState !== 'idle') return
+		const search = new URL(readCurrentRouterHref(handle), 'http://localhost')
+			.search
 		resetSelectionState()
-		replaceLocation(platformIntegrationsRoute.buildNewHref())
+		replaceLocation(platformIntegrationsRoute.buildNewHref(search))
 		handle.update()
 	}

Apply the same search argument at Line 392 and Line 428.

πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function cancelEditor() {
resetSelectionState()
replaceLocation(platformIntegrationsRoute.buildListHref())
handle.update()
}
const primaryButtonCss = getPillButtonCss({ size: 'sm' })
const secondaryButtonCss = getGhostButtonCss({ size: 'sm' })
const dangerButtonCss = getDangerPillCss({ size: 'sm' })
function startCreateIntegration() {
if (actionState !== 'idle') return
resetSelectionState()
replaceLocation(platformIntegrationsRoute.buildNewHref())
handle.update()
}
function cancelEditor() {
const search = new URL(readCurrentRouterHref(handle), 'http://localhost')
.search
resetSelectionState()
replaceLocation(platformIntegrationsRoute.buildListHref(search))
handle.update()
}
function startCreateIntegration() {
if (actionState !== 'idle') return
const search = new URL(readCurrentRouterHref(handle), 'http://localhost')
.search
resetSelectionState()
replaceLocation(platformIntegrationsRoute.buildNewHref(search))
handle.update()
}
πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/client/routes/admin-platform-integrations.tsx` around lines
434 - 445, Preserve the active search query across integration navigation by
passing the current search string to the route builders in the post-create
navigation, handleDelete, cancelEditor, and startCreateIntegration. Reuse the
same search value already supplied by the row links with buildListHref() and
buildNewHref(), so q remains intact when leaving or creating an integration.

Comment on lines +857 to +869
const needsStaleRefresh =
consumeStaleNavigationData(currentHref) && !routeData
const needsLoad =
(status === 'loading' ||
currentHref !== lastLoadedHref ||
needsStaleRefresh) &&
currentHref !== lastFailedHref &&
loadingForHref !== currentHref
if (!routeData && needsLoad && typeof document !== 'undefined') {
status = 'loading'
loadingForHref = currentHref
handle.queueTask(loadPlatformIntegrations)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | πŸ”΄ Critical | πŸ—οΈ Heavy lift

Typing in the search field refetches the list and unmounts the table.

RecordTableSearch.onInput calls replaceLocation on every keystroke. replaceLocation dispatches the router navigate event, so the render function runs again with a new currentHref that carries the updated q.

In that render, needsLoad at Line 859 is true because currentHref !== lastLoadedHref. Line 866 then sets status = 'loading' and Line 868 queues loadPlatformIntegrations. The RecordTable is gated on status === 'ready' at Line 905, so the whole table β€” including the focused search input β€” is removed from the DOM on the first keystroke, and the field loses focus. Each keystroke also refetches the full integrations list, although filterApps already filters client-side and the API response does not depend on q.

Key the load gate on the part of the URL the payload actually depends on, not on the full href. The list payload is the same for the list, new, and detail paths and for every value of q.

πŸ› Proposed fix: compare a payload-relevant key instead of the full href
+	/** The list payload is identical for every path and query on this screen. */
+	const dataHref = adminPlatformIntegrationsApiPath

Then use dataHref where the load gate currently compares hrefs:

 		const needsStaleRefresh =
 			consumeStaleNavigationData(currentHref) && !routeData
 		const needsLoad =
 			(status === 'loading' ||
-				currentHref !== lastLoadedHref ||
+				dataHref !== lastLoadedHref ||
 				needsStaleRefresh) &&
-			currentHref !== lastFailedHref &&
-			loadingForHref !== currentHref
+			dataHref !== lastFailedHref &&
+			loadingForHref !== dataHref
 		if (!routeData && needsLoad && typeof document !== 'undefined') {
 			status = 'loading'
-			loadingForHref = currentHref
+			loadingForHref = dataHref
 			handle.queueTask(loadPlatformIntegrations)
 		}

Apply the same key to the assignments inside loadPlatformIntegrations (Lines 181-222) and to Lines 854-855.

If you prefer to keep the href-based gate, then keep the previous data on screen during a refetch instead of blanking it: render the RecordTable whenever apps.length > 0 and pass busy for the in-flight state, which is what the busy prop exists for.

Also applies to: 919-930

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/client/routes/admin-platform-integrations.tsx` around lines
857 - 869, Key platform-integrations loading state to the payload-relevant
dataHref rather than the full currentHref, so query updates and list/detail
navigation do not refetch or unmount the table. Update the needsLoad comparisons
and the corresponding assignments inside loadPlatformIntegrations, including the
nearby render logic, while preserving normal loading for genuinely different
payloads.

@kentcdodds
kentcdodds merged commit 44b3c9c into main Aug 10, 2026
18 checks passed
@kentcdodds
kentcdodds deleted the cursor/packages-table-scroll-platform-integrations-7faa branch August 10, 2026 02:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants