Skip to content

feat: Phase 2 — ObjectUI Rendering Engine#6

Merged
hotlong merged 3 commits into
mainfrom
copilot/start-development-phase-2
Feb 8, 2026
Merged

feat: Phase 2 — ObjectUI Rendering Engine#6
hotlong merged 3 commits into
mainfrom
copilot/start-development-phase-2

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Feb 8, 2026

Implements the metadata-driven rendering engine (Phase 2 of ROADMAP.md) that interprets ObjectUI schemas from the server and renders native mobile components.

View Renderers (components/renderers/)

  • ViewRenderer — Top-level dispatcher with extensible registry (registerRenderer() for future kanban/calendar/chart types)
  • ListViewRenderer — Derives columns from view metadata or field definitions, sort chips, pull-to-refresh, field-type formatting
  • FormViewRenderer — Section-based layout from FormViewMeta, required-field validation, conditional visibility via visibleOn, wired to useMutation()
  • DetailViewRenderer — Read-only sectioned display with related lists and action bar (edit/delete/custom)
  • DashboardViewRenderer — Widget dispatcher for metric, card, list, and chart widget types
  • FieldRenderer — Maps all ObjectQL field types to native inputs with read-only and editable modes
// Top-level dispatcher routes to the correct renderer
<ViewRenderer viewType="list" props={{ view, fields, records, onRowPress }} />

// Or use renderers directly
<ListViewRenderer view={listViewMeta} fields={fields} records={records} />
<FormViewRenderer fields={fields} onSubmit={handleSubmit} />

Action System (components/actions/)

  • ActionExecutor — Executes url/api/flow/modal action types with {field} template resolution
  • ActionBar — Horizontal scrollable toolbar rendering actions by variant
  • FloatingActionButton — FAB with haptic feedback

Route Integration

  • [objectName]/index.tsx → now uses ListViewRenderer with useFields() + useView()
  • [objectName]/[id].tsx → now uses DetailViewRenderer with action bar
  • New: [objectName]/new.tsx — create form via useMutation('create')
  • New: [objectName]/[id]/edit.tsx — edit form via useMutation('update')

Types

components/renderers/types.ts defines local interfaces (ListViewMeta, FormViewMeta, FormSection, FormFieldMeta, DashboardMeta, ActionMeta, FieldDefinition) mirroring @objectstack/spec/ui shapes for stable renderer contracts.

Remaining Phase 2 items (not in scope)

  • Filter drawer UI, swipe actions, row selection (2.2)
  • Record navigation prev/next (2.4)
  • Responsive widget grid, live data query wiring (2.5)

✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI and others added 2 commits February 8, 2026 05:00
- ViewRenderer: top-level dispatcher with extensible registry
- ListViewRenderer: metadata-driven list with sorting, pull-to-refresh, field formatting
- FormViewRenderer: section-based forms with FieldRenderer, validation, mutation
- DetailViewRenderer: read-only record display with action bar, related lists
- DashboardViewRenderer: metric, card, list, chart widget types
- FieldRenderer: maps 40+ ObjectQL field types to native inputs
- Action system: ActionExecutor, ActionBar, FloatingActionButton
- Wire renderers into existing app routes (object list, detail)
- Add new routes: create (new.tsx) and edit ([id]/edit.tsx)
- Update ROADMAP.md with Phase 2 progress

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
…essages

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Copilot AI changed the title [WIP] Initiate development for phase 2 feat: Phase 2 — ObjectUI Rendering Engine Feb 8, 2026
Copilot AI requested a review from hotlong February 8, 2026 05:05
@hotlong hotlong marked this pull request as ready for review February 8, 2026 06:49
Copilot AI review requested due to automatic review settings February 8, 2026 06:49
@hotlong hotlong merged commit 8e2e494 into main Feb 8, 2026
2 checks passed
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

Implements Phase 2’s metadata-driven ObjectUI Rendering Engine and wires it into the dynamic object routes, enabling server-driven list/form/detail/dashboard rendering plus an initial action execution system.

Changes:

  • Added renderer suite (ViewRenderer, list/form/detail/dashboard renderers, FieldRenderer) with shared local type contracts for ObjectUI metadata.
  • Introduced an action system (ActionExecutor, ActionBar, FloatingActionButton) for executing metadata-defined actions.
  • Updated object routes and added create/edit routes to use the new renderers and SDK hooks.

Reviewed changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
docs/ROADMAP.md Marks Phase 2 renderer/action milestones as complete.
components/renderers/types.ts Adds local renderer-facing ObjectUI metadata/type contracts.
components/renderers/index.ts Central export surface for renderers and shared types.
components/renderers/ViewRenderer.tsx Dispatcher + extensible registry for view-type renderers.
components/renderers/ListViewRenderer.tsx Metadata-driven list rendering with sort chips/search/refresh/infinite scroll.
components/renderers/FormViewRenderer.tsx Section-based form rendering + basic required validation + conditional visibility.
components/renderers/DetailViewRenderer.tsx Read-only record rendering with action bar + related lists.
components/renderers/DashboardViewRenderer.tsx Metadata-driven dashboard widget dispatcher (metric/card/list/chart placeholder).
components/renderers/fields/FieldRenderer.tsx Field-type-to-native input mapping + display formatting helpers.
components/actions/index.ts Central export surface for action components/utilities.
components/actions/ActionExecutor.ts Executes url/api/flow/modal action types with basic templating.
components/actions/ActionBar.tsx Horizontal toolbar UI for rendering action buttons.
components/actions/FloatingActionButton.tsx FAB component with haptics.
app/(app)/[appName]/[objectName]/index.tsx Swaps legacy list UI for ListViewRenderer + useFields/useView.
app/(app)/[appName]/[objectName]/[id].tsx Swaps legacy detail UI for DetailViewRenderer + adds edit navigation.
app/(app)/[appName]/[objectName]/new.tsx Adds create route using FormViewRenderer + useMutation('create').
app/(app)/[appName]/[objectName]/[id]/edit.tsx Adds edit route using FormViewRenderer + useMutation('update').

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

Comment on lines +169 to +188
/* ---- Error state ---- */
if (error && !isLoading) {
return (
<View className="flex-1 items-center justify-center px-6">
<Text className="text-base text-destructive">{error.message}</Text>
{onRefresh && (
<Pressable
className="mt-4 rounded-xl bg-primary px-5 py-3"
onPress={onRefresh}
>
<Text className="font-semibold text-primary-foreground">Retry</Text>
</Pressable>
)}
</View>
);
}

/* ---- Search state ---- */
const [searchQuery, setSearchQuery] = useState("");

Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

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

useState for searchQuery is declared after an early return (if (error && !isLoading) return …). This violates the Rules of Hooks because the number/order of hooks changes when error is present, and will crash or be flagged by eslint-plugin-react-hooks. Move the useState (and any other hooks) above all conditional returns, or refactor error rendering so hooks are always called.

Copilot uses AI. Check for mistakes.
Comment on lines +34 to +39
if ((type === "url" || type === "email") && typeof value === "string" && value) {
const href = type === "email" ? `mailto:${value}` : value;
return (
<Pressable onPress={() => Linking.openURL(href)}>
<Text className="text-base text-primary underline">{display}</Text>
</Pressable>
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

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

Linking.openURL(href) is fired from onPress without handling rejections. If the URL cannot be opened, this can cause an unhandled promise rejection. Wrap the call in void Linking.openURL(href).catch(...) (and/or Linking.canOpenURL) to handle failures gracefully.

Copilot uses AI. Check for mistakes.
multiline={multiline}
textAlignVertical={multiline ? "top" : "center"}
placeholderTextColor="#9ca3af"
autoCapitalize={field.type === "email" ? "none" : "sentences"}
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

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

Password fields render through TextFieldInput, but the TextInput isn’t configured for sensitive input (e.g., secureTextEntry) and autoCapitalize will default to "sentences". Use secureTextEntry and disable autocapitalize/autocorrect for field.type === "password".

Suggested change
autoCapitalize={field.type === "email" ? "none" : "sentences"}
secureTextEntry={field.type === "password"}
autoCapitalize={
field.type === "email" || field.type === "password" ? "none" : "sentences"
}
autoCorrect={field.type === "password" ? false : undefined}

Copilot uses AI. Check for mistakes.
Comment on lines +244 to +248
case "multiselect":
case "checkboxes":
case "tags":
return <SelectFieldInput {...props} />;

Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

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

multiselect / checkboxes / tags are routed to SelectFieldInput, but Select only supports a single string value. This means multi-valued fields can’t be edited correctly (arrays will be stringified and only one option can be chosen). Implement a multi-select UI (or a chips/checkbox list) and use an array value contract for these field types.

Copilot uses AI. Check for mistakes.
Comment on lines +3 to +4
import { Edit, Trash2, MoreHorizontal } from "lucide-react-native";
import { cn } from "~/lib/utils";
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

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

Unused imports (MoreHorizontal, cn) are present, which will fail linting under the current ESLint config. Remove them or use them.

Suggested change
import { Edit, Trash2, MoreHorizontal } from "lucide-react-native";
import { cn } from "~/lib/utils";
import { Edit, Trash2 } from "lucide-react-native";

Copilot uses AI. Check for mistakes.
contentContainerStyle={{ padding: 16, paddingBottom: 40 }}
refreshControl={
onRefresh ? (
<RefreshControl refreshing={false} onRefresh={onRefresh} />
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

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

RefreshControl is hard-coded with refreshing={false}, so pull-to-refresh will never show the refreshing indicator even while onRefresh is running. Track a local isRefreshing state (especially if onRefresh returns a Promise) or accept an isRefreshing prop and pass it through here.

Suggested change
<RefreshControl refreshing={false} onRefresh={onRefresh} />
<RefreshControl refreshing={isLoading} onRefresh={onRefresh} />

Copilot uses AI. Check for mistakes.
@@ -0,0 +1,42 @@
import React from "react";
import { Pressable, View } from "react-native";
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

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

View is imported but never used, which will fail linting. Remove the unused import.

Suggested change
import { Pressable, View } from "react-native";
import { Pressable } from "react-native";

Copilot uses AI. Check for mistakes.
Comment on lines +8 to +9
const { appName, objectName } = useLocalSearchParams<{
appName: string;
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

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

appName is destructured from useLocalSearchParams but never used, which will fail linting. Remove it from the destructuring (or use it for navigation if intended).

Suggested change
const { appName, objectName } = useLocalSearchParams<{
appName: string;
const { objectName } = useLocalSearchParams<{

Copilot uses AI. Check for mistakes.
Comment on lines +10 to +11
const { appName, objectName, id } = useLocalSearchParams<{
appName: string;
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

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

appName is destructured from useLocalSearchParams but never used, which will fail linting. Remove it from the destructuring (or use it if needed).

Suggested change
const { appName, objectName, id } = useLocalSearchParams<{
appName: string;
const { objectName, id } = useLocalSearchParams<{

Copilot uses AI. Check for mistakes.
Comment on lines 16 to +18
const { data: schema } = useObject(objectName!);
const { data: viewData } = useView(objectName!, "form");
const { data: fieldsData } = useFields(objectName!);
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

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

schema from useObject(objectName!) is never used. This will fail linting and also triggers an unnecessary query; remove the hook/variable or use it (e.g., to derive display fields).

Copilot uses AI. Check for mistakes.
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.

3 participants