Skip to content

fix(app-shell): inbox/activity "see all" links open in an app the user can open (#4074) - #4242

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-4074-inbox-links-app-resolve
Aug 11, 2026
Merged

fix(app-shell): inbox/activity "see all" links open in an app the user can open (#4074)#4242
yinlianghui merged 1 commit into
mainfrom
claude/issue-4074-inbox-links-app-resolve

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#4074

Four navigation producers hardcoded /apps/setup/... for two pages that are not bound to the setup app. sys_inbox_message and sys_activity are framework-owned objects reachable at /apps/{any app}/{object}, exactly like system/approvals — whose entry in the same popover, one line above, was already resolving the current app (objectstack#7234). The in-code comment defending the hardcoded target argued the OBJECT is app-independent, which is true, and which is precisely why it renders under whatever app the user can open rather than under setup.

Both browser verifications on the card measured the cost, and it is two defects, not one:

  • a business user whose app list does not contain setup gets the target rendered inside their own app's shell with a "You don't have access" empty state (softer, and more confusing, than a hard app guard);
  • every user, admins included, is switched out of the app they were in — URL, sidebar and header app switcher all flip to Setup, with no announcement and no way back except the app switcher.

The change

One shared helper, resolveHostAppSegment in packages/app-shell/src/utils/appRoute.ts, generalizing the resolution objectstack#7231 landed inline in HomePage for the approvals entry. Home and the bell popover both call it, so the two surfaces cannot drift into different answers to the same question. Resolution order:

  1. the app the user is in / last had open, re-checked against the live active-app list (a deactivated or hidden app is not resurrected as a link);
  2. their first active app;
  3. the caller's own hint, unchecked — but only when the list names no app at all, i.e. it has not loaded yet. An empty list is "unknown" at least as often as "this user has no apps", and demoting a user who is demonstrably rendering inside /apps/crm/... to setup would reintroduce this very defect in a loading race;
  4. setup last — what is left is a degenerate app carrying neither _packageId nor name, so the historical target beats a broken link.

The four anchors now resolve through it:

ProducerBeforeAfter
HomePageonOpenNotification fallback (no action_url)/apps/setup/sys_inbox_message?view=mine/apps/{host}/sys_inbox_message?view=mine
HomePageHomeActivity onViewAll/apps/setup/sys_activity/apps/{host}/sys_activity
InboxPopovergoToAllNotifications/apps/setup/sys_inbox_message?view=mine/apps/{host}/sys_inbox_message?view=mine
InboxPopovergoToAllActivity/apps/setup/sys_activity/apps/{host}/sys_activity

HomePage's inline activeApps predicate moved into the same module as filterActiveApps, so "an app the user can open" means one thing to Home's launcher and to every producer building a link into an app.

Deliberately unchanged, and pinned as such

  • /apps/setup/system/marketplace and /apps/setup/system/apps — admin-scoped surfaces where setup is the target rather than an oversight (the card's own scope guard). A case asserts the marketplace link still emits /apps/setup/system/marketplace.
  • goToApprovals and handleNotificationClick in the popover — untouched; cases assert they still resolve the current app and still follow an explicit action_url.
  • A user whose current/last-open app IS setup still lands in setup. The fallback order's tail is not "never setup", it is "an app this user can actually open".

Verification

Red-first, then reverse-verified. With the four sites reverted on top of the finished branch (helper left in place), 13 of 63 cases went red — every case asserting a resolved target — while all 12 helper unit cases and every CONTROL stayed green, which is the intended split: the helper is right in isolation, the four anchors are the wiring. Restored: 63 passed.

# red-first, before the fix
Tests 6 failed | 3 passed (9) HomePage.inboxLinksTarget.test.tsx
Tests 7 failed | 6 passed (13) InboxPopover.viewAllTarget.test.tsx
AssertionError: expected '/apps/setup/sys_activity' to be '/apps/crm/sys_activity'
# after
pnpm exec vitest run packages/app-shell/
Test Files 337 passed (337)
Tests 3205 passed | 1 skipped (3206)
pnpm --filter @object-ui/app-shell type-check -> clean
pnpm --filter @object-ui/app-shell lint -> 0 errors (2273 pre-existing warnings)
node scripts/check-control-bytes.mjs -> OK
node scripts/check-changeset-presence.mjs -> 1 changeset declared

No copy changed — the four edits are navigation targets and comments only — so there is no i18n work in this PR. Changeset: @object-ui/app-shell patch.

What this fix does NOT close — do not misread the empty state

objectstack#7344 is a second, independent cause: no shipped permission set grants sys_inbox_message (member_default, the additive everyone baseline, does not name it). Re-pointing these four anchors at an app the user can open therefore leaves them on the same "You don't have access" state until that grant question is settled — verified on the card against the Account app's own Notifications entry, which reaches sys_inbox_message with no setup involved anywhere and renders the identical empty state.

The routing fix is necessary, not sufficient. Accordingly every case here asserts the resolved target — the argument navigate() receives — and none asserts what renders at the far end; pinning the far end would pin someone else's defect.


Generated by Claude Code

…r can open (#4074)
Four producers hardcoded `/apps/setup/...` for pages that are not bound to
the setup app: Home's notification fallback and "View all activity", and the
bell popover's two footer drills. `sys_inbox_message` and `sys_activity` are
framework-owned objects reachable at `/apps/{any app}/{object}` — exactly like
`system/approvals`, whose entry in the same popover already resolved the
current app. Being app-independent is why they render under whatever app the
user can open, not a reason to render them in `setup`.
Measured in a browser on the issue: a business user without setup access gets
"You don't have access" (the shell renders the target inside their own app),
and every user is switched out of the app they were in — URL, sidebar and app
switcher flip to Setup with no announcement.
All four now resolve through one shared helper, `resolveHostAppSegment` in
`utils/appRoute.ts`, generalizing the resolution objectstack#7231 introduced
for the approvals entry so Home and the popover cannot answer the same
question differently: current/last-open app re-checked against the live
active-app list, then the first active app, then the caller's own hint when
the list has not loaded, then `setup`.
The admin-scoped `/apps/setup/system/marketplace` and `/apps/setup/system/apps`
links are deliberately untouched and pinned as such.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
@vercel

vercelBot commented Aug 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectuiIgnoredIgnoredAug 11, 2026 6:17am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)28.3 KB350 KB
Entry fileindex-CGyD2QqX.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)8.88KB3.25KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)7.57KB2.97KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)22.10KB4.37KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.13KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.64KB2.21KB
auth (SocialSignInButtons.js)9.60KB3.89KB
auth (UserMenu.js)3.40KB1.22KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)35.76KB9.11KB
auth (createAuthenticatedFetch.js)4.37KB1.69KB
auth (index.js)2.35KB1.07KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)4.91KB0.87KB
auth (useIsWorkspaceAdmin.js)1.61KB0.85KB
collaboration (CommentThread.js)26.07KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.65KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)488.60KB108.25KB
core (index.js)3.04KB1.15KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)144.34KB37.61KB
fields (index.js)228.33KB56.58KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.32KB1.77KB
i18n (index.js)2.65KB1.06KB
i18n (pickLocalized.js)1.70KB0.83KB
i18n (provider.js)9.48KB3.27KB
i18n (useObjectLabel.js)27.59KB6.63KB
i18n (useSafeTranslation.js)4.52KB1.96KB
layout (index.js)38.98KB10.85KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.74KB
mobile (index.js)1.50KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.71KB0.42KB
mobile (useResponsiveConfig.js)1.36KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)8.75KB3.06KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)3.67KB1.12KB
permissions (evaluator.js)4.41KB1.44KB
permissions (index.js)0.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
permissions (usePermissions.js)1.55KB0.71KB
plugin-ai (index.js)15.71KB3.79KB
plugin-calendar (index.js)45.23KB12.45KB
plugin-charts (index.js)61.52KB17.49KB
plugin-chatbot (index.js)180.33KB42.79KB
plugin-dashboard (index.js)118.58KB30.71KB
plugin-designer (index.js)210.85KB42.64KB
plugin-detail (index.js)238.53KB59.65KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)114.58KB27.68KB
plugin-gantt (index.js)164.14KB39.98KB
plugin-grid (index.js)187.97KB49.90KB
plugin-kanban (index.js)48.60KB13.41KB
plugin-list (index.js)110.31KB26.76KB
plugin-map (index.js)17.00KB5.32KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)40.58KB10.58KB
plugin-timeline (index.js)26.21KB7.52KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.03KB20.55KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.71KB3.53KB
providers (index.js)0.44KB0.22KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.67KB2.37KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)23.71KB7.96KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.23KB0.66KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)4.09KB1.74KB
sdui-parser (index.js)4.47KB2.03KB
sdui-parser (parse.js)10.04KB2.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)4.69KB1.48KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-retry.js)4.32KB2.02KB
types (index.js)2.71KB1.34KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@yinlianghui
yinlianghui marked this pull request as ready for review August 11, 2026 06:26
@yinlianghui
yinlianghui added this pull request to the merge queueAug 11, 2026
Merged via the queue into main with commit f812de6Aug 11, 2026
21 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-4074-inbox-links-app-resolve branch August 11, 2026 06:26
os-help added a commit that referenced this pull request Aug 11, 2026
One conflict, in `packages/app-shell/src/console/home/HomePage.tsx`, between
two changes to the same home-grid app filter:
- main (#4242 / objectui#4074) extracted the predicate
`active !== false && hidden !== true` into a shared `filterActiveApps()` in
`utils/appRoute.ts`, so Home's launcher and every producer that builds a link
into an app answer "an app the user can open" identically;
- this branch (objectstack#6955) added a ⛔ note to that same inline predicate
recording that it must keep filtering `hidden` and must NEVER gain an
`_unpublished` clause.
Both intents are kept and neither is behavioral, so this is not a trade-off:
HomePage takes main's `filterActiveApps(apps)` call, and the ⛔ note moves onto
`filterActiveApps` itself. The pin is strictly stronger there — it is now the
single definition, so it covers Home's grid AND `resolveHostAppSegment`'s link
producers, which main newly routed through the same predicate.
`filterActiveApps` does not read `_unpublished`, so the #6955 contract is
unchanged by the merge: the banner still reads `_unpublished`, the per-app
publish body is still `{"_unpublished": false}`, and every launcher surface
still filters `hidden` alone. The ⛔ pin gains a test case in
`utils/__tests__/appRoute.test.ts` alongside the existing AppSwitcher and
RootLandingRedirect pins; reverse-verified by injecting the guarded clause into
`filterActiveApps` (that case, and only that case, went red).
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants

@yinlianghui@claude