Skip to content

ENG-261#361

Merged
Connorbelez merged 7 commits intomainfrom
Connorbelez/eng-261-linear
Mar 31, 2026
Merged

ENG-261#361
Connorbelez merged 7 commits intomainfrom
Connorbelez/eng-261-linear

Conversation

@Connorbelez
Copy link
Copy Markdown
Owner

@Connorbelez Connorbelez commented Mar 31, 2026

Summary by CodeRabbit

Release Notes

  • New Features
    • Added CRM demo playground with custom object creation and management
    • Enabled record creation, viewing, and linking between records
    • Introduced table and kanban views for visualizing records
    • Added system adapter support for viewing native system records alongside custom data
    • Included demo data seeding and reset capabilities for testing

@linear
Copy link
Copy Markdown

linear bot commented Mar 31, 2026

Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Sorry @Connorbelez, your pull request is larger than the review limit of 150000 diff characters

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 31, 2026

Warning

Rate limit exceeded

@Connorbelez has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 14 minutes and 22 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 14 minutes and 22 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 25accc88-2454-4654-817c-98889f19a38b

📥 Commits

Reviewing files that changed from the base of the PR and between 7bcf859 and 9f48922.

📒 Files selected for processing (9)
  • convex/crm/__tests__/records.test.ts
  • convex/crm/__tests__/viewEngine.test.ts
  • convex/demo/crmSandbox.ts
  • src/components/demo/crm/DynamicRecordForm.tsx
  • src/components/demo/crm/FieldInput.tsx
  • src/components/demo/crm/ObjectCreator.tsx
  • src/components/demo/crm/ObjectInventoryCard.tsx
  • src/components/demo/crm/RecordTableSurface.tsx
  • src/routes/demo/crm/index.tsx
🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.48% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'ENG-261' is a ticket/issue identifier but does not meaningfully describe the primary changes in the changeset, which involve extensive CRM demo implementation, backend record querying, and sandbox tooling. Expand the title to include a brief description of the main feature or change (e.g., 'ENG-261: CRM demo sandbox and record detail implementation' or 'ENG-261: Add CRM playground with object creation and record views').
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch Connorbelez/eng-261-linear

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 and usage tips.

@Connorbelez Connorbelez marked this pull request as ready for review March 31, 2026 16:05
Copilot AI review requested due to automatic review settings March 31, 2026 16:05
Copy link
Copy Markdown
Owner Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

Copy link
Copy Markdown

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds an ENG-261 “EAV-CRM Integration Sandbox” under /demo/crm to exercise CRM control-plane + data-plane flows end-to-end (custom objects, system adapters, links) with lightweight instrumentation and demo seed/reset helpers. This PR also extends the Convex CRM backend to better support native/system-object parity in view queries and record detail lookups.

Changes:

  • Introduces new /demo/crm route group (layout + 3 tabs) and a set of reusable demo-local CRM components (object creator, record composer, table surface, detail surface, link explorer, metrics panel).
  • Adds Convex demo helpers (demo/crmSandbox) for seeding/resetting demo CRM data.
  • Extends CRM backend with recordQueries.getRecordReference and enables viewQueries.queryViewRecords to load system/native records via the system adapter.

Reviewed changes

Copilot reviewed 43 out of 45 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/routeTree.gen.ts Updates generated route tree to include /demo/crm routes.
src/routes/demo/crm/route.tsx Adds CRM sandbox layout, navigation, and metrics wrapper.
src/routes/demo/crm/index.tsx Implements “Custom Objects” tab (seed/reset, object inventory, record composer, preview, detail).
src/routes/demo/crm/system.tsx Implements “System Adapters” tab using shared table/detail surfaces.
src/routes/demo/crm/links.tsx Implements “Link Explorer” tab for link types, linking records, and linked-record inspection.
src/components/demo/crm/MetricsProvider.tsx Adds shared metrics context for demo surfaces.
src/components/demo/crm/ValidationMetrics.tsx Adds sticky metrics panel UI (reads, render time, record-shape match).
src/components/demo/crm/RecordTableSurface.tsx Adds shared “UnifiedRecord” table preview surface + metrics hooks.
src/components/demo/crm/RecordDetailCard.tsx Adds shared record-detail card using getRecordReference.
src/components/demo/crm/ObjectCreator.tsx Adds object + field creation UI using a demo action wrapper.
src/components/demo/crm/ObjectInventoryCard.tsx Adds object inventory selection card UI.
src/components/demo/crm/FieldDefEditor.tsx Adds field-definition builder UI.
src/components/demo/crm/SelectOptionsEditor.tsx Adds select/multi-select option editor UI.
src/components/demo/crm/FieldInput.tsx Adds field-type → input control mapping for record composer.
src/components/demo/crm/DynamicRecordForm.tsx Adds record composer form that submits to crm.records.createRecord.
src/components/demo/crm/functionRefs.ts Adds typed function references for demo endpoints + link APIs.
src/components/demo/crm/utils.ts Adds demo utilities: error extraction, slugify, formatting, EAV read estimation, shape checking.
src/components/demo/crm/formatters.ts Adds formatting helpers for metrics display.
src/components/demo/crm/schema.ts Adds demo-side schema/type helpers for field drafting.
src/components/demo/crm/types.ts Adds demo shared types (record kind, metrics state, seed summary).
convex/demo/crmSandbox.ts Adds seed/reset + sandbox object creation demo endpoints.
convex/crm/viewQueries.ts Adds native/system-object record loading path for view queries.
convex/crm/systemAdapters/queryAdapter.ts Refactors native record assembly; adds getNativeRecordById.
convex/crm/recordQueries.ts Adds getRecordReference query supporting both EAV and native records + link loading.
convex/crm/tests/helpers.test.ts Introduces shared CRM test harness + seed helpers.
convex/crm/tests/walkthrough.test.ts Updates CRM tests to use new helpers module name.
convex/crm/tests/viewEngine.test.ts Updates CRM tests to use new helpers module name.
convex/crm/tests/systemAdapters.test.ts Updates CRM tests to use new helpers module name.
convex/crm/tests/records.test.ts Updates CRM tests to use new helpers module name.
convex/crm/tests/metadataCompiler.test.ts Updates CRM tests to use new helpers module name.
convex/_generated/api.d.ts Updates generated API typings for new/added modules.
specs/ENG-261/tasks.md Adds overall ENG-261 task plan + repo reality notes.
specs/ENG-261/chunks/manifest.md Adds chunk breakdown/manifest for ENG-261 work.
specs/ENG-261/chunks/chunk-01-foundation/tasks.md Adds chunk task list.
specs/ENG-261/chunks/chunk-01-foundation/context.md Adds chunk context/constraints.
specs/ENG-261/chunks/chunk-02-custom-object-playground/tasks.md Adds chunk task list.
specs/ENG-261/chunks/chunk-02-custom-object-playground/context.md Adds chunk context/constraints.
specs/ENG-261/chunks/chunk-03-record-views/tasks.md Adds chunk task list.
specs/ENG-261/chunks/chunk-03-record-views/context.md Adds chunk context/constraints.
specs/ENG-261/chunks/chunk-04-record-surfaces/tasks.md Adds chunk task list.
specs/ENG-261/chunks/chunk-04-record-surfaces/context.md Adds chunk context/constraints.
specs/ENG-261/chunks/chunk-05-system-detail/tasks.md Adds chunk task list.
specs/ENG-261/chunks/chunk-05-system-detail/context.md Adds chunk context/constraints.
specs/ENG-261/chunks/chunk-06-links-polish/tasks.md Adds chunk task list.
specs/ENG-261/chunks/chunk-06-links-polish/context.md Adds chunk context/constraints.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread convex/demo/crmSandbox.ts
Comment thread src/components/demo/crm/FieldInput.tsx
Comment thread src/components/demo/crm/DynamicRecordForm.tsx
Comment thread convex/crm/recordQueries.ts
Comment thread src/components/demo/crm/RecordTableSurface.tsx Outdated
Comment thread convex/crm/viewQueries.ts
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 11

🧹 Nitpick comments (7)
src/components/demo/crm/utils.ts (1)

130-139: Timezone handling inconsistency: local time vs UTC.

The toDateInputValue function uses local time methods (getFullYear(), getMonth(), getHours(), etc.), but the relevant code snippet from the same file shows a version using UTC methods (getUTCFullYear(), getUTCMonth(), etc.). This inconsistency could cause timezone-related bugs when editing date/datetime values.

For datetime-local inputs, using local time is actually correct since the input type expects local time. However, for date inputs (line 139), toISOString().slice(0, 10) returns UTC date which may differ from local date near midnight.

♻️ Consistent date formatting
-	return date.toISOString().slice(0, 10);
+	const year = date.getFullYear();
+	const month = `${date.getMonth() + 1}`.padStart(2, "0");
+	const day = `${date.getDate()}`.padStart(2, "0");
+	return `${year}-${month}-${day}`;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/demo/crm/utils.ts` around lines 130 - 139, The
toDateInputValue function mixes local-time getters for the "datetime" branch
with a UTC-based return (date.toISOString().slice(0, 10)) for the date branch,
causing timezone drift; update the date branch to format year/month/day using
local getters (getFullYear(), getMonth(), getDate()) and pad month/day like the
"datetime" branch so both branches consistently use local time for date inputs
and datetime-local formatting.
src/components/demo/crm/FieldInput.tsx (1)

110-154: Consider emitting undefined when multi_select becomes empty.

When unchecking the last option, the component emits an empty array [] (line 133-135). However, sanitizeRecordValues in DynamicRecordForm.tsx (line 42) filters out empty arrays. This works correctly end-to-end, but for consistency with other field types that emit undefined for empty values, consider:

 onChange(
-	currentValues.filter((item) => item !== option.value)
+	currentValues.length === 1 && currentValues[0] === option.value
+		? undefined
+		: currentValues.filter((item) => item !== option.value)
 );

This is optional since the current behavior is handled downstream.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/demo/crm/FieldInput.tsx` around lines 110 - 154, The
multi_select branch in FieldInput.tsx currently calls onChange([]) when the last
checkbox is unchecked; change the onCheckedChange logic inside the Checkbox in
the "multi_select" case so that if removing the option results in an empty
currentValues array it calls onChange(undefined) instead of onChange([]),
otherwise call onChange([...]) or onChange(filteredArray) as appropriate; update
the handler that currently filters currentValues (the anonymous function passed
to onCheckedChange) to compute the newValues and emit undefined when
newValues.length === 0 to match other field types and sanitizeRecordValues
behavior.
src/components/demo/crm/FieldDefEditor.tsx (1)

256-265: Consider improving the rich text preview placeholder.

The disabled textarea with value="" shows only a placeholder. This is fine for a demo, but if intended as a preview, consider showing sample formatted content or removing it if it adds no value to the field builder UX.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/demo/crm/FieldDefEditor.tsx` around lines 256 - 265, The rich
text preview in FieldDefEditor.tsx (the branch checking field.fieldType ===
"rich_text") currently renders a disabled Textarea with value="" which only
shows the placeholder; update this to either (a) render meaningful sample
formatted content in the Textarea (e.g., sample paragraphs, lists, and inline
formatting) so the Preview Label shows what rich text will look like, or (b)
remove the Preview block entirely if no preview value is desired; adjust the JSX
around the Label and Textarea accordingly and ensure the Textarea value is
populated from a sample string or the preview block is removed to improve UX.
src/components/demo/crm/DynamicRecordForm.tsx (1)

70-92: Consider adding required field validation before submission.

The form allows submission without validating that required fields have values. While the Convex mutation will reject invalid records, providing client-side feedback would improve UX:

♻️ Add client-side required field validation
 async function handleSubmit() {
   if (!objectDefId) {
     return;
   }

+  const missingRequired = activeFields
+    .filter((f) => f.isRequired && values[f.name] === undefined)
+    .map((f) => f.label);
+  if (missingRequired.length > 0) {
+    toast.error(`Required fields missing: ${missingRequired.join(", ")}`);
+    return;
+  }

   setIsSubmitting(true);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/demo/crm/DynamicRecordForm.tsx` around lines 70 - 92, The
handleSubmit function currently allows submitting without client-side
required-field checks; before calling setIsSubmitting or sanitizeRecordValues,
validate activeFields for any field marked required (e.g., inspect activeFields
entries for a required flag or isRequired) and ensure corresponding keys in
values are present and non-empty (trim strings, check arrays/objects
appropriately); if any required fields are missing, call toast.error with a
concise message listing the missing field labels and return early without
calling createRecord or changing state; keep the rest of the flow
(setIsSubmitting, sanitizeRecordValues, createRecord, setValues,
onRecordCreated, setIsSubmitting(false)) unchanged when validation passes.
src/routes/demo/crm/route.tsx (1)

12-15: Consider lazy-loading the /demo/crm route component for improved code-splitting.

This route currently uses an inline component property. Per guidelines, file-based routes should use lazy-loaded components via lazyRouteComponent or a .lazy.tsx companion file for code-splitting. Moving CrmSandboxLayout to a lazy variant would keep this non-critical route out of the initial bundle.

Example pattern: Create src/routes/demo/crm/route.lazy.tsx with the component, then reference it via lazyRouteComponent in the main route file.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/routes/demo/crm/route.tsx` around lines 12 - 15, The Route currently sets
component: CrmSandboxLayout inline; convert this to a lazy-loaded component by
moving the CrmSandboxLayout export into a companion file (e.g., create
src/routes/demo/crm/route.lazy.tsx that exports the component) and update the
Route created by createFileRoute to use lazyRouteComponent pointing at that lazy
file; specifically, replace the component property with lazyRouteComponent(...)
referencing the new route.lazy.tsx export so the /demo/crm route is code-split
and not included in the initial bundle.
convex/demo/crmSandbox.ts (2)

215-236: Function name doesn't reflect its behavior.

deleteFieldGraph deletes field capabilities but returns the fieldDefs for the caller to delete separately (see Line 510-512). Consider renaming to deleteFieldCapabilitiesAndReturnFields or having the function delete the fieldDefs itself for consistency with other delete*Graph functions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@convex/demo/crmSandbox.ts` around lines 215 - 236, The function
deleteFieldGraph currently only removes related fieldCapabilities but returns
the fieldDefs for the caller to remove, so rename or change behavior for
consistency: either rename deleteFieldGraph to
deleteFieldCapabilitiesAndReturnFields (update all call sites, e.g., where
deleteFieldGraph is invoked around lines that later delete fieldDefs) to make
its contract explicit, or modify deleteFieldGraph to also delete the returned
fieldDefs (delete each fieldDef._id after deleting its capabilities) so it
behaves like other delete*Graph helpers; pick one approach and update the
function name/usages and tests accordingly (refer to function deleteFieldGraph,
returned variable fieldDefs, and related table fieldCapabilities).

135-163: Consider parallelizing value table deletions.

Each value table is queried and deleted sequentially. For records with many values, this could be slow. Since these deletions are independent per table, they could run in parallel.

♻️ Suggested parallel deletion
 async function deleteValueRowsForRecord(
 	ctx: MutationCtx,
 	recordId: Id<"records">
 ) {
-	for (const table of VALUE_TABLES) {
-		switch (table) {
-			case "recordValuesText":
-			case "recordValuesNumber":
-			case "recordValuesBoolean":
-			case "recordValuesDate":
-			case "recordValuesSelect":
-			case "recordValuesMultiSelect":
-			case "recordValuesRichText":
-			case "recordValuesUserRef": {
-				const rows = await ctx.db
-					.query(table)
-					.withIndex("by_record", (q) => q.eq("recordId", recordId))
-					.collect();
-				for (const row of rows) {
-					await ctx.db.delete(row._id);
-				}
-				break;
-			}
-			default: {
-				throw new ConvexError(`Unsupported value table: ${String(table)}`);
-			}
-		}
-	}
+	await Promise.all(
+		VALUE_TABLES.map(async (table) => {
+			const rows = await ctx.db
+				.query(table)
+				.withIndex("by_record", (q) => q.eq("recordId", recordId))
+				.collect();
+			await Promise.all(rows.map((row) => ctx.db.delete(row._id)));
+		})
+	);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@convex/demo/crmSandbox.ts` around lines 135 - 163, The current
deleteValueRowsForRecord function iterates VALUE_TABLES sequentially which is
slow; change it to run per-table deletions in parallel by mapping VALUE_TABLES
to an array of async tasks and awaiting Promise.all. For each table (inside
deleteValueRowsForRecord) keep the same withIndex query
(ctx.db.query(table).withIndex("by_record", q => q.eq("recordId",
recordId)).collect()), then delete the returned rows concurrently (e.g., map row
=> ctx.db.delete(row._id) and await Promise.all for that table), and throw the
ConvexError for unknown tables as before; ensure the function still returns
after all table-level deletions complete.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@convex/crm/__tests__/metadataCompiler.test.ts`:
- Line 20: The test imports helpers from "./helpers.test", which violates the
Biome noExportsInTest rule; change the import back to "./helpers" and ensure the
shared helper file is named helpers.ts (not helpers.test.ts) so exported
utilities are in a non-.test file; update the import in
convex/crm/__tests__/metadataCompiler.test.ts to reference "./helpers" and, if
needed, rename the helper module file and update any other tests importing it
(look for the helpers module symbol used by the tests to verify all references
are updated).

In `@convex/crm/__tests__/systemAdapters.test.ts`:
- Line 21: The test import was changed to "./helpers.test" which violates
Biome's noExportsInTest rule; revert the import in systemAdapters.test.ts back
to import from "./helpers" (replace "./helpers.test" with "./helpers") and
ensure the shared helper module is named/exported from helpers.ts (not
helpers.test.ts) so exported utilities used by tests come from a non-.test.ts
file (check exported symbols in that helpers module match what's imported here).

In `@convex/crm/__tests__/viewEngine.test.ts`:
- Line 11: The import in viewEngine.test.ts currently points to "./helpers.test"
which violates Biome's noExportsInTest rule; update the import to use
"./helpers" and ensure the referenced helper module file is renamed from
helpers.test.ts to helpers.ts (and retains its exported helpers), then update
any other tests importing "./helpers.test" to the new "./helpers" path so shared
test utilities are not exported from a .test.ts file.

In `@convex/crm/__tests__/walkthrough.test.ts`:
- Around line 9-13: Rename the shared test helper file from helpers.test.ts to
helpers.ts and update all imports that reference it (e.g., imports of asAdmin,
CrmTestHarness, createCrmTestHarness) to point to "./helpers" instead of
"./helpers.test"; ensure any other test files referencing helpers.test.ts are
updated to import from the new helpers module so the file uses a .ts extension
and satisfies the noExportsInTest rule.

In `@src/components/demo/crm/ObjectCreator.tsx`:
- Around line 60-67: The current submit validation in ObjectCreator.tsx only
checks options.length for select/multi_select fields so a blank option can slip
through; update the validation for fields (the fields array check using
field.fieldType and field.options) to ensure every option has non-empty label
and value (e.g., no option where !option.label.trim() or !option.value.trim()),
and treat any such option as invalid to block submit; also ensure this aligns
with the payload builder logic that reads options (the code around the payload
builder on Lines ~87-91) so it will no longer send empty label/value strings.

In `@src/components/demo/crm/ObjectInventoryCard.tsx`:
- Around line 45-55: The interactive row in ObjectInventoryCard is visually
marked via isActive but lacks accessibility state; update the button element
(the one using objectDef._id, onSelect, isActive) to expose selection to AT by
adding an aria-pressed attribute set to the boolean isActive (or use appropriate
listbox semantics/role and aria-selected if you prefer listbox patterns),
ensuring the attribute toggles when onClick triggers onSelect so screen readers
announce the selected state.

In `@src/components/demo/crm/RecordTableSurface.tsx`:
- Around line 84-103: The timer reset currently happens whenever preview ===
undefined (startedAtRef.current = performance.now()), causing renderTimeMs to
measure only from the last loading render; instead, initialize/reset
startedAtRef.current when the underlying view/data actually changes (e.g., when
tablePreview becomes available or when tablePreview/fields identity changes).
Remove the preview-undefined assignment and set startedAtRef.current inside the
useEffect (or a separate effect) that depends on tablePreview and fields (or the
specific view/object identifier), so that
setRenderTime(Math.round(performance.now() - startedAtRef.current)) in the
existing useEffect reflects time since the real view/data change; reference
startedAtRef, tablePreview, fields, and setRenderTime in your change.
- Around line 181-193: The TableRow elements rendered with TableRow, cn,
onSelectRecord, selectedRecordId, row._id and row._kind are clickable but not
keyboard-accessible; make them focusable by adding tabIndex={0}, give an
appropriate role (e.g., role="button" or role="row" with aria-selected when
selected), and implement an onKeyDown handler on the same element that calls
onSelectRecord with { recordId: row._id, recordKind: row._kind } when Enter or
Space is pressed (mirror the existing onClick behavior). Ensure you only add
these props when onSelectRecord is provided and keep the visual focus style
consistent with the current selectedRecordId handling.

In `@src/routes/demo/crm/index.tsx`:
- Around line 231-246: Both buttons currently only disable themselves (using
isSeeding and isResetting), allowing concurrent mutating actions; change them so
both use a shared busy flag (e.g., compute const isBusy = isSeeding ||
isResetting or directly use isSeeding || isResetting) and pass that to each
Button's disabled prop so both "Seed lead pipeline" and "Reset sandbox objects"
are disabled while either onSeed or onReset is running.
- Around line 141-147: When the user selects a different object in
ObjectInventoryCard the previous record selection must be cleared so
RecordDetailCard doesn't query with a stale selectedRecord; update the onSelect
handler (currently passing setSelectedObjectId) to call both
setSelectedObjectId(newId) and clear the record selection (e.g.,
setSelectedRecordId(null) and/or setSelectedRecordKind(null) or
setSelectedRecord(null)) so selectedRecord state is reset whenever the inventory
selection changes.
- Around line 53-63: The effect that resets selectedObjectId is deriving a
fallback from possibly stale customObjects, causing selection bounce after
handleSeed() or onCreated(); change the logic in the useEffect so it never picks
customObjects[0]?._id from the current (stale) query snapshot as a fallback:
only set selectedObjectId when there is no selection and use
seedState?.demoObjectId if present, or prefer an explicit pendingSelection id
set by handleSeed()/onCreated (e.g. store the new id in a ref or state like
pendingSelectionRef and read that here), and otherwise do nothing until the live
customObjects list updates — keep references to selectedObjectId, customObjects,
seedState?.demoObjectId, setSelectedObjectId, handleSeed(), and onCreated() when
implementing this change.

---

Nitpick comments:
In `@convex/demo/crmSandbox.ts`:
- Around line 215-236: The function deleteFieldGraph currently only removes
related fieldCapabilities but returns the fieldDefs for the caller to remove, so
rename or change behavior for consistency: either rename deleteFieldGraph to
deleteFieldCapabilitiesAndReturnFields (update all call sites, e.g., where
deleteFieldGraph is invoked around lines that later delete fieldDefs) to make
its contract explicit, or modify deleteFieldGraph to also delete the returned
fieldDefs (delete each fieldDef._id after deleting its capabilities) so it
behaves like other delete*Graph helpers; pick one approach and update the
function name/usages and tests accordingly (refer to function deleteFieldGraph,
returned variable fieldDefs, and related table fieldCapabilities).
- Around line 135-163: The current deleteValueRowsForRecord function iterates
VALUE_TABLES sequentially which is slow; change it to run per-table deletions in
parallel by mapping VALUE_TABLES to an array of async tasks and awaiting
Promise.all. For each table (inside deleteValueRowsForRecord) keep the same
withIndex query (ctx.db.query(table).withIndex("by_record", q =>
q.eq("recordId", recordId)).collect()), then delete the returned rows
concurrently (e.g., map row => ctx.db.delete(row._id) and await Promise.all for
that table), and throw the ConvexError for unknown tables as before; ensure the
function still returns after all table-level deletions complete.

In `@src/components/demo/crm/DynamicRecordForm.tsx`:
- Around line 70-92: The handleSubmit function currently allows submitting
without client-side required-field checks; before calling setIsSubmitting or
sanitizeRecordValues, validate activeFields for any field marked required (e.g.,
inspect activeFields entries for a required flag or isRequired) and ensure
corresponding keys in values are present and non-empty (trim strings, check
arrays/objects appropriately); if any required fields are missing, call
toast.error with a concise message listing the missing field labels and return
early without calling createRecord or changing state; keep the rest of the flow
(setIsSubmitting, sanitizeRecordValues, createRecord, setValues,
onRecordCreated, setIsSubmitting(false)) unchanged when validation passes.

In `@src/components/demo/crm/FieldDefEditor.tsx`:
- Around line 256-265: The rich text preview in FieldDefEditor.tsx (the branch
checking field.fieldType === "rich_text") currently renders a disabled Textarea
with value="" which only shows the placeholder; update this to either (a) render
meaningful sample formatted content in the Textarea (e.g., sample paragraphs,
lists, and inline formatting) so the Preview Label shows what rich text will
look like, or (b) remove the Preview block entirely if no preview value is
desired; adjust the JSX around the Label and Textarea accordingly and ensure the
Textarea value is populated from a sample string or the preview block is removed
to improve UX.

In `@src/components/demo/crm/FieldInput.tsx`:
- Around line 110-154: The multi_select branch in FieldInput.tsx currently calls
onChange([]) when the last checkbox is unchecked; change the onCheckedChange
logic inside the Checkbox in the "multi_select" case so that if removing the
option results in an empty currentValues array it calls onChange(undefined)
instead of onChange([]), otherwise call onChange([...]) or
onChange(filteredArray) as appropriate; update the handler that currently
filters currentValues (the anonymous function passed to onCheckedChange) to
compute the newValues and emit undefined when newValues.length === 0 to match
other field types and sanitizeRecordValues behavior.

In `@src/components/demo/crm/utils.ts`:
- Around line 130-139: The toDateInputValue function mixes local-time getters
for the "datetime" branch with a UTC-based return (date.toISOString().slice(0,
10)) for the date branch, causing timezone drift; update the date branch to
format year/month/day using local getters (getFullYear(), getMonth(), getDate())
and pad month/day like the "datetime" branch so both branches consistently use
local time for date inputs and datetime-local formatting.

In `@src/routes/demo/crm/route.tsx`:
- Around line 12-15: The Route currently sets component: CrmSandboxLayout
inline; convert this to a lazy-loaded component by moving the CrmSandboxLayout
export into a companion file (e.g., create src/routes/demo/crm/route.lazy.tsx
that exports the component) and update the Route created by createFileRoute to
use lazyRouteComponent pointing at that lazy file; specifically, replace the
component property with lazyRouteComponent(...) referencing the new
route.lazy.tsx export so the /demo/crm route is code-split and not included in
the initial bundle.
🪄 Autofix (Beta)

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

Run ID: c09d12a9-f7ae-4d91-9f99-585aa3e815a3

📥 Commits

Reviewing files that changed from the base of the PR and between 5e57691 and 7bcf859.

⛔ Files ignored due to path filters (1)
  • convex/_generated/api.d.ts is excluded by !**/_generated/**
📒 Files selected for processing (44)
  • convex/crm/__tests__/helpers.test.ts
  • convex/crm/__tests__/metadataCompiler.test.ts
  • convex/crm/__tests__/records.test.ts
  • convex/crm/__tests__/systemAdapters.test.ts
  • convex/crm/__tests__/viewEngine.test.ts
  • convex/crm/__tests__/walkthrough.test.ts
  • convex/crm/recordQueries.ts
  • convex/crm/systemAdapters/queryAdapter.ts
  • convex/crm/viewQueries.ts
  • convex/demo/crmSandbox.ts
  • specs/ENG-261/chunks/chunk-01-foundation/context.md
  • specs/ENG-261/chunks/chunk-01-foundation/tasks.md
  • specs/ENG-261/chunks/chunk-02-custom-object-playground/context.md
  • specs/ENG-261/chunks/chunk-02-custom-object-playground/tasks.md
  • specs/ENG-261/chunks/chunk-03-record-views/context.md
  • specs/ENG-261/chunks/chunk-03-record-views/tasks.md
  • specs/ENG-261/chunks/chunk-04-record-surfaces/context.md
  • specs/ENG-261/chunks/chunk-04-record-surfaces/tasks.md
  • specs/ENG-261/chunks/chunk-05-system-detail/context.md
  • specs/ENG-261/chunks/chunk-05-system-detail/tasks.md
  • specs/ENG-261/chunks/chunk-06-links-polish/context.md
  • specs/ENG-261/chunks/chunk-06-links-polish/tasks.md
  • specs/ENG-261/chunks/manifest.md
  • specs/ENG-261/tasks.md
  • src/components/demo/crm/DynamicRecordForm.tsx
  • src/components/demo/crm/FieldDefEditor.tsx
  • src/components/demo/crm/FieldInput.tsx
  • src/components/demo/crm/MetricsProvider.tsx
  • src/components/demo/crm/ObjectCreator.tsx
  • src/components/demo/crm/ObjectInventoryCard.tsx
  • src/components/demo/crm/RecordDetailCard.tsx
  • src/components/demo/crm/RecordTableSurface.tsx
  • src/components/demo/crm/SelectOptionsEditor.tsx
  • src/components/demo/crm/ValidationMetrics.tsx
  • src/components/demo/crm/formatters.ts
  • src/components/demo/crm/functionRefs.ts
  • src/components/demo/crm/schema.ts
  • src/components/demo/crm/types.ts
  • src/components/demo/crm/utils.ts
  • src/routeTree.gen.ts
  • src/routes/demo/crm/index.tsx
  • src/routes/demo/crm/links.tsx
  • src/routes/demo/crm/route.tsx
  • src/routes/demo/crm/system.tsx

Comment thread convex/crm/__tests__/metadataCompiler.test.ts Outdated
Comment thread convex/crm/__tests__/systemAdapters.test.ts Outdated
Comment thread convex/crm/__tests__/viewEngine.test.ts Outdated
Comment thread convex/crm/__tests__/walkthrough.test.ts Outdated
Comment thread src/components/demo/crm/ObjectCreator.tsx
Comment thread src/components/demo/crm/RecordTableSurface.tsx
Comment thread src/components/demo/crm/RecordTableSurface.tsx
Comment thread src/routes/demo/crm/index.tsx
Comment thread src/routes/demo/crm/index.tsx
Comment thread src/routes/demo/crm/index.tsx Outdated
@Connorbelez Connorbelez changed the title sandbox ENG-2611 Mar 31, 2026
@Connorbelez Connorbelez changed the title ENG-2611 ENG-261 Mar 31, 2026
@Connorbelez Connorbelez merged commit 14804e9 into main Mar 31, 2026
1 of 3 checks passed
Connorbelez added a commit that referenced this pull request Apr 20, 2026
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

## Release Notes

* **New Features**
  * Added CRM demo playground with custom object creation and management
  * Enabled record creation, viewing, and linking between records
  * Introduced table and kanban views for visualizing records
  * Added system adapter support for viewing native system records alongside custom data
  * Included demo data seeding and reset capabilities for testing

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
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