Conversation
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
… workflow, realtime, notifications Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
…nt comments, improve type safety Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR completes Phase 4B ObjectOS integration by adopting the SDK v2.0.1 typed APIs and adding end-to-end support for views, permissions, workflows, realtime, and notifications across hooks, UI components, and documentation.
Changes:
- Refactors saved views storage to use typed
client.views.*APIs and supports list/form view configs (with legacy flat filter/sort/columns mapping). - Introduces new hooks for permissions, workflow state, realtime subscriptions/presence, and notifications (plus corresponding UI/screens).
- Updates roadmap/project status docs and expands hook test coverage.
Reviewed changes
Copilot reviewed 19 out of 20 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| hooks/useWorkflowState.ts | New hook wrapping client.workflow.* for state/transition/approve/reject with refetch. |
| hooks/useViewStorage.ts | Refactor to typed client.views.*, add list/form config support + legacy mapping helpers. |
| hooks/useSubscription.ts | New realtime hook for connect/subscribe + presence helpers. |
| hooks/usePermissions.ts | New hook for object/field permissions and permission checks. |
| hooks/useNotifications.ts | New notifications hook: list/pagination, mark read, device registration, preferences. |
| components/workflow/WorkflowStatePanel.tsx | Workflow UI panel for state badge, transitions, approval actions, and history. |
| components/renderers/ListViewRenderer.tsx | Adds allowCreate prop (permission-related). |
| components/renderers/FormViewRenderer.tsx | Adds fieldPermissions and applies editable to readonly logic. |
| components/renderers/DetailViewRenderer.tsx | Adds allowEdit/allowDelete and gates action handlers accordingly. |
| components/realtime/CollaborationIndicator.tsx | Presence-based collaboration indicator component. |
| components/actions/ActionBar.tsx | Adds disabledActions filtering to hide unauthorized actions. |
| app/(tabs)/notifications.tsx | Implements notifications list UI with read/unread state, mark-all-read, and deep linking. |
| tests/hooks/useWorkflowState.test.ts | Tests workflow hook fetch/transition/approve/reject. |
| tests/hooks/useViewStorage.test.ts | Tests typed views API integration + legacy mapping behavior. |
| tests/hooks/useSubscription.test.ts | Tests realtime connect/subscribe and presence helpers. |
| tests/hooks/usePermissions.test.ts | Tests permission fetching and checkPermission helper. |
| tests/hooks/useNotifications.test.ts | Tests notifications list, read management, device registration, preferences, pagination. |
| docs/ROADMAP.md | Marks Phase 4B.1–4B.5 as completed. |
| docs/PROJECT-STATUS.md | Updates Phase 4B status to COMPLETE and refreshes test counts/status text. |
| .gitignore | Ignores package-lock.json (pnpm repo). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const isFieldReadonly = readonly || meta.readonly || | ||
| (fieldPermissions?.[fieldDef.name] && !fieldPermissions[fieldDef.name].editable); | ||
|
|
There was a problem hiding this comment.
Field-level permissions include readable, but the renderer only uses editable to decide read-only state. As-is, fields with { readable: false } will still be rendered (data exposure) and only made read-only. Consider filtering such fields out (or rendering a placeholder) when readable is false.
| // Keep callback ref stable | ||
| const onEventRef = useRef(onEvent); | ||
| onEventRef.current = onEvent; | ||
|
|
There was a problem hiding this comment.
onEvent is accepted and stored in a ref, but it is never invoked anywhere in the hook. Either remove the option or wire it to the SDK’s event stream so callers actually receive realtime events.
| // Auto-connect on mount if enabled; reconnect only when enabled/channel change. | ||
| // doConnect/doDisconnect are intentionally omitted to avoid re-subscribing on | ||
| // every render when the events array or callbacks change. | ||
| useEffect(() => { | ||
| if (enabled) { | ||
| void doConnect(); | ||
| } | ||
| return () => { | ||
| void doDisconnect(); | ||
| }; | ||
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| }, [enabled, channel]); |
There was a problem hiding this comment.
The effect intentionally omits doDisconnect from the dependency list, but that causes the cleanup to capture a stale subscriptionId (usually null from first render). On unmount/channel change this will skip unsubscribe(subscriptionId) and can leak server-side subscriptions. Use a ref for the latest subscriptionId/disconnect handler or include doDisconnect in deps and restructure to avoid resubscribe loops.
| const result: RealtimeConnectResponse = await client.realtime.connect({ | ||
| transport: "websocket", | ||
| channels: [channel], | ||
| }); | ||
| setConnectionId(result.connectionId); | ||
| setIsConnected(true); | ||
|
|
||
| // Subscribe to the channel | ||
| const sub: RealtimeSubscribeResponse = await client.realtime.subscribe({ | ||
| channel, | ||
| events, | ||
| }); | ||
| setSubscriptionId(sub.subscriptionId); | ||
| } catch (err) { | ||
| setError( | ||
| err instanceof Error ? err : new Error("Realtime connection failed"), | ||
| ); | ||
| setIsConnected(false); | ||
| } |
There was a problem hiding this comment.
If client.realtime.subscribe() throws after a successful connect(), the catch block sets isConnected false but never disconnects the underlying realtime connection. Ensure you clean up (unsubscribe/disconnect) on partial failures to avoid leaving a live connection around.
| setNotifications((prev) => | ||
| prev.map((n) => (ids.includes(n.id) ? { ...n, read: true } : n)), | ||
| ); | ||
| setUnreadCount((prev) => Math.max(0, prev - ids.length)); |
There was a problem hiding this comment.
unreadCount is decremented by ids.length regardless of whether those notifications were previously unread or even present in state. This can desync the count from reality. Prefer recomputing unreadCount from the updated notifications list, or use the server response (e.g., readCount/unreadCount) if available.
| setNotifications((prev) => | |
| prev.map((n) => (ids.includes(n.id) ? { ...n, read: true } : n)), | |
| ); | |
| setUnreadCount((prev) => Math.max(0, prev - ids.length)); | |
| setNotifications((prev) => { | |
| const updated = prev.map((n) => | |
| ids.includes(n.id) ? { ...n, read: true } : n, | |
| ); | |
| const newUnreadCount = updated.reduce( | |
| (count, n) => (n.read ? count : count + 1), | |
| 0, | |
| ); | |
| setUnreadCount(newUnreadCount); | |
| return updated; | |
| }); |
| onPress, | ||
| }: { | ||
| transition: WorkflowTransition; | ||
| onPress: (t: WorkflowTransition) => void; | ||
| }) { | ||
| return ( | ||
| <Pressable | ||
| className="flex-row items-center gap-1.5 rounded-lg border border-border px-3 py-2 active:bg-muted/50" | ||
| onPress={() => onPress(transition)} |
There was a problem hiding this comment.
While an action is in flight (isActing), transition buttons remain enabled, so users can trigger duplicate transitions rapidly. Disable the TransitionButton pressable (and optionally approve/reject) when isActing is true.
| onPress, | |
| }: { | |
| transition: WorkflowTransition; | |
| onPress: (t: WorkflowTransition) => void; | |
| }) { | |
| return ( | |
| <Pressable | |
| className="flex-row items-center gap-1.5 rounded-lg border border-border px-3 py-2 active:bg-muted/50" | |
| onPress={() => onPress(transition)} | |
| onPress, | |
| isActing, | |
| }: { | |
| transition: WorkflowTransition; | |
| onPress: (t: WorkflowTransition) => void; | |
| isActing?: boolean; | |
| }) { | |
| return ( | |
| <Pressable | |
| className="flex-row items-center gap-1.5 rounded-lg border border-border px-3 py-2 active:bg-muted/50" | |
| onPress={() => onPress(transition)} | |
| disabled={isActing} |
| /** Permission: hide create button when false */ | ||
| allowCreate?: boolean; |
There was a problem hiding this comment.
allowCreate is declared in props but never read/used (and is not referenced anywhere in this component). Either wire it into the list UI (e.g., hide/disable the create affordance) or remove the prop to avoid a misleading API surface.
| /** Permission: hide create button when false */ | |
| allowCreate?: boolean; |
Implements all Phase 4B ObjectOS integration features using the SDK v2.0.1 typed APIs. Five subsystems: views refactor, permissions, workflow/approval, realtime subscriptions, and push notifications.
4B.1 — Views API Refactor
(client as any).viewsworkaround inuseViewStorage.ts; now uses typedclient.views.*API directlySavedViewtype extended withlist/formview config fields matching SDK'sListViewsResponsefilters/sort/columnsinputs auto-wrapped into list config4B.2 — Permission System
hooks/usePermissions.ts— wrapsclient.permissions.getObjectPermissions()and.check()ListViewRenderer:allowCreateDetailViewRenderer:allowEdit,allowDeleteFormViewRenderer:fieldPermissions(per-field{ readable, editable })ActionBar:disabledActionsset to filter unauthorized actions4B.3 — Workflow & Approval
hooks/useWorkflowState.ts—getState(),transition(),approve(),reject()with auto-refetchcomponents/workflow/WorkflowStatePanel.tsx— state badge, transition buttons, approve/reject with comment input, collapsible history timeline4B.4 — Real-time Updates
hooks/useSubscription.ts— connection lifecycle, channel subscriptions, presence viaclient.realtime.*components/realtime/CollaborationIndicator.tsx— avatar row showing who's viewing/editing4B.5 — Push Notifications
hooks/useNotifications.ts— list, markRead, markAllRead, registerDevice, preferences CRUDapp/(tabs)/notifications.tsxupgraded from empty stub to full notification list with read/unread state, mark-all-read, and deep linking viaactionUrlTests
💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.