Show admin-only modal for non-admin billing access#1479
Conversation
…nagement Instead of hiding the billing and admin tabs for non-admin users, all tabs are now visible. When non-admins click on admin-only tabs (billing, usage, plans, credits, audit-logs, security), the content is blurred and a modal appears showing which admins they need to contact. This provides better UX by allowing non-admins to discover these features while preventing access with a clear explanation. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
📝 WalkthroughWalkthroughAdds admin-only access UI: new AdminOnlyModal component, settings layout checks to show the modal for non-admins (including billing click handling and content blur), TypeScript component declarations, and localized strings in many message files. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant Settings as SettingsLayout
participant Store as OrganizationStore
participant Modal as AdminOnlyModal
User->>Settings: Navigate to settings / click Billing tab
Settings->>Store: Read user role / org info
Settings->>Settings: Evaluate isSuperAdmin / isOnAdminOnlyRoute
alt non-admin accessing admin-only route or billing
Settings->>Modal: open AdminOnlyModal
Modal->>Store: fetch organization members
Store-->>Modal: return members list
Modal->>Modal: filter admins (super_admin/admin), map emails & avatars
Modal-->>User: render modal with admin contacts (or loading spinner)
else admin allowed
Settings-->>User: proceed to normal settings flow (open portal or show content)
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. 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. Comment |
Added translations for: - admin-only-access - admin-only-billing-description - contact-your-admin Languages: de, es, fr, hi, id, it, ja, ko, pl, pt-br, ru, tr, vi, zh-cn Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@src/layouts/settings.vue`:
- Around line 27-34: The adminOnlyRoutes array currently contains two incorrect
audit-log entries ('/settings/organization/audit-logs' and
'/settings/organization/auditlogs') that don't match the registered route;
update the adminOnlyRoutes array (symbol: adminOnlyRoutes in settings.vue) to
replace both entries with the correct route string
'/settings/organization/AuditLogs' so the admin-only access check will match the
actual route.
🧹 Nitpick comments (3)
src/components/AdminOnlyModal.vue (2)
13-26: Consider handling the empty admins state.If fetching fails or no admins are found, the user sees no contact information. Consider adding an error state or fallback message.
💡 Suggested improvement
const admins = ref<{ email: string, image_url: string }[]>([]) const isLoading = ref(true) +const hasError = ref(false) onMounted(async () => { try { const members = await organizationStore.getMembers() admins.value = members .filter(m => m.role === 'super_admin' || m.role === 'admin') .map(m => ({ email: m.email, image_url: m.image_url })) } catch (e) { console.error('Failed to fetch admins:', e) + hasError.value = true } finally { isLoading.value = false } })Then in the template, add a fallback when
admins.length === 0 && !isLoading:<div v-else-if="admins.length === 0" class="mt-4 text-sm text-gray-500 dark:text-gray-400"> {{ t('contact-your-admin') }} </div>
29-42: Consider adding accessibility attributes to the modal.The modal overlay could benefit from semantic attributes for screen readers.
♿ Accessibility improvement
- <div class="flex absolute inset-0 z-10 flex-col justify-center items-center bg-white/60 dark:bg-gray-900/60"> - <div class="p-8 text-center bg-white rounded-xl border shadow-xl dark:bg-gray-800 border-blue-200 dark:border-blue-700 max-w-md"> + <div + class="flex absolute inset-0 z-10 flex-col justify-center items-center bg-white/60 dark:bg-gray-900/60" + role="dialog" + aria-modal="true" + aria-labelledby="admin-only-title" + > + <div class="p-8 text-center bg-white rounded-xl border shadow-xl dark:bg-gray-800 border-blue-200 dark:border-blue-700 max-w-md">And add an
idto the heading:- <h2 class="mb-2 text-2xl font-bold text-gray-900 dark:text-white"> + <h2 id="admin-only-title" class="mb-2 text-2xl font-bold text-gray-900 dark:text-white">src/layouts/settings.vue (1)
150-153: Consider improving the billing modal dismissal UX.The billing modal (line 153) dismisses when clicking anywhere on it, which may not be intuitive. Users might accidentally dismiss it before reading the admin contacts. Consider adding an explicit close button inside the
AdminOnlyModalcomponent or passing a prop to enable a close button.Alternatively, you could add
@click.stopon the inner modal content inAdminOnlyModal.vueand only dismiss when clicking the backdrop:<!-- In settings.vue --> <AdminOnlyModal v-if="showBillingModal" `@click.self`="showBillingModal = false" />This would allow clicks on the modal content (admin list) without dismissing it.
| const adminOnlyRoutes = [ | ||
| '/settings/organization/usage', | ||
| '/settings/organization/plans', | ||
| '/settings/organization/credits', | ||
| '/settings/organization/audit-logs', | ||
| '/settings/organization/auditlogs', | ||
| '/settings/organization/security', | ||
| ] |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -type f -name "*router*" -o -name "*routes*" | grep -v node_modules | head -20Repository: Cap-go/capgo
Length of output: 79
🏁 Script executed:
rg -n "audit" --type=ts --type=js -g '!node_modules' -g '!*.d.ts' | head -30Repository: Cap-go/capgo
Length of output: 2930
🏁 Script executed:
rg -n "path.*audit|audit.*path" --type=ts --type=js --type=vue -g '!node_modules' | head -30Repository: Cap-go/capgo
Length of output: 83
🏁 Script executed:
cat -n ./src/typed-router.d.ts | grep -i "audit" -A 2 -B 2Repository: Cap-go/capgo
Length of output: 539
🏁 Script executed:
fd -e ts -e js "router\|routes" src/ | grep -v node_modules | grep -v distRepository: Cap-go/capgo
Length of output: 38
🏁 Script executed:
rg "audit" --type ts -g '!node_modules' -g '!*.d.ts' -C 3 | grep -i "path\|route" -A 3 -B 3Repository: Cap-go/capgo
Length of output: 38
🏁 Script executed:
cat -n ./src/pages/settings/organization/AuditLogs.vue | head -20Repository: Cap-go/capgo
Length of output: 911
🏁 Script executed:
rg "/settings/organization.*[Aa]udit" --type ts --type vue -g '!node_modules'Repository: Cap-go/capgo
Length of output: 83
🏁 Script executed:
rg "/settings/organization" --type ts -g '!node_modules' -g '!*.d.ts' | grep -i auditRepository: Cap-go/capgo
Length of output: 174
🏁 Script executed:
rg "audit" -i --type ts -g '!node_modules' -g '!*.d.ts' | grep -E "path|route|/settings"Repository: Cap-go/capgo
Length of output: 174
🏁 Script executed:
cat -n ./src/constants/organizationTabs.tsRepository: Cap-go/capgo
Length of output: 1453
🏁 Script executed:
cat -n ./src/typed-router.d.ts | grep -i "audit" -C 5Repository: Cap-go/capgo
Length of output: 940
🏁 Script executed:
cat -n ./src/typed-router.d.ts | grep "/settings/organization/" | head -20Repository: Cap-go/capgo
Length of output: 1137
🏁 Script executed:
cat -n ./src/layouts/settings.vue | head -50Repository: Cap-go/capgo
Length of output: 2288
🏁 Script executed:
rg "adminOnlyRoutes" --type ts --type vue -g '!node_modules'Repository: Cap-go/capgo
Length of output: 83
Replace both audit log route entries with the correct path.
The adminOnlyRoutes array contains /settings/organization/audit-logs and /settings/organization/auditlogs, but the actual registered route is /settings/organization/AuditLogs (CamelCase). The current entries will not match the actual route, breaking the admin-only access check. Replace both with /settings/organization/AuditLogs.
🤖 Prompt for AI Agents
In `@src/layouts/settings.vue` around lines 27 - 34, The adminOnlyRoutes array
currently contains two incorrect audit-log entries
('/settings/organization/audit-logs' and '/settings/organization/auditlogs')
that don't match the registered route; update the adminOnlyRoutes array (symbol:
adminOnlyRoutes in settings.vue) to replace both entries with the correct route
string '/settings/organization/AuditLogs' so the admin-only access check will
match the actual route.
There was a problem hiding this comment.
Pull request overview
This PR improves UX by making admin-only features discoverable to non-admin users. Instead of hiding billing and subscription-related tabs, they are now always visible. When non-admin users click on these tabs or navigate to admin-only routes, a modal appears showing which admins they need to contact.
Changes:
- Removed complex conditional logic that showed/hid tabs based on permissions
- Added AdminOnlyModal component that displays admin contact information
- Tabs are now always visible with permission checks happening at interaction time
- Added blur effect and modal overlay for admin-only routes
Reviewed changes
Copilot reviewed 17 out of 18 changed files in this pull request and generated 8 comments.
| File | Description |
|---|---|
| src/layouts/settings.vue | Removed 65+ lines of conditional tab visibility logic; added admin-only route detection and modal triggering; billing tab now always shown with click-time permission check |
| src/components/AdminOnlyModal.vue | New modal component that fetches and displays organization admins when non-admin users attempt to access restricted features |
| messages/en.json | Added three translation keys for the admin-only modal UI |
| src/components.d.ts | Auto-generated TypeScript definitions for the new AdminOnlyModal component |
| "admin-dashboard-construction": "Admin dashboard is under construction. Components will be added in the next phase.", | ||
| "admin-dashboard-description": "Platform-wide statistics and analytics", | ||
| "admin-only-access": "Admin Access Required", | ||
| "admin-only-billing-description": "Only organization admins can manage billing and subscriptions. Please contact an admin to make changes.", |
There was a problem hiding this comment.
The translation key 'admin-only-billing-description' is used for all admin-only features (usage, plans, credits, audit logs, security), not just billing. This message specifically mentions "billing and subscriptions" which is incorrect when shown for other admin-only routes. Consider renaming to 'admin-only-description' and updating the message to be more generic, such as "Only organization admins can access this feature. Please contact an admin for assistance."
| "admin-only-billing-description": "Only organization admins can manage billing and subscriptions. Please contact an admin to make changes.", | |
| "admin-only-billing-description": "Only organization admins can access this feature. Please contact an admin for assistance.", |
| <main class="flex relative flex-1 w-full min-h-0 mt-0 overflow-hidden bg-blue-50 dark:bg-slate-800/40"> | ||
| <div | ||
| class="flex-1 w-full min-h-0 px-0 pt-0 mx-auto mb-8 overflow-y-auto sm:px-6 md:pt-16 lg:px-8 max-w-9xl" | ||
| :class="{ 'blur-sm pointer-events-none select-none': showAdminOnlyModal }" |
There was a problem hiding this comment.
The blur effect is only applied when showAdminOnlyModal is true, but the billing modal (showBillingModal) at line 153 also overlays the content without applying blur. For consistency in UX, consider applying the same blur effect when showBillingModal is true, or update the blur condition to include both modal states.
| :class="{ 'blur-sm pointer-events-none select-none': showAdminOnlyModal }" | |
| :class="{ 'blur-sm pointer-events-none select-none': showAdminOnlyModal || showBillingModal }" |
| watchEffect(() => { | ||
| // ensure usage/plans tabs based on permissions (keeps icons from base) | ||
| const needsUsage = organizationStore.hasPermissionsInRole(organizationStore.currentRole, ['super_admin']) | ||
| const hasUsage = organizationTabs.value.find(tab => tab.key === '/settings/organization/usage') | ||
| if (needsUsage && !hasUsage) { | ||
| const base = baseOrgTabs.find(t => t.key === '/settings/organization/usage') | ||
| if (base) | ||
| organizationTabs.value.push({ ...base }) | ||
| } | ||
| if (!needsUsage && hasUsage) | ||
| organizationTabs.value = organizationTabs.value.filter(tab => tab.key !== '/settings/organization/usage') | ||
|
|
||
| const needsCredits = organizationStore.hasPermissionsInRole(organizationStore.currentRole, ['super_admin']) | ||
| const hasCredits = organizationTabs.value.find(tab => tab.key === '/settings/organization/credits') | ||
|
|
||
| if (needsCredits && !hasCredits) { | ||
| const base = baseOrgTabs.find(t => t.key === '/settings/organization/credits') | ||
| if (base) | ||
| organizationTabs.value.push({ ...base }) | ||
| } | ||
|
|
||
| if (!needsCredits && hasCredits) | ||
| organizationTabs.value = organizationTabs.value.filter(tab => tab.key !== '/settings/organization/credits') | ||
|
|
||
| const needsPlans = organizationStore.hasPermissionsInRole(organizationStore.currentRole, ['super_admin']) | ||
| const hasPlans = organizationTabs.value.find(tab => tab.key === '/settings/organization/plans') | ||
| if (needsPlans && !hasPlans) { | ||
| const base = baseOrgTabs.find(t => t.key === '/settings/organization/plans') | ||
| if (base) | ||
| organizationTabs.value.push({ ...base }) | ||
| } | ||
| if (!needsPlans && hasPlans) | ||
| organizationTabs.value = organizationTabs.value.filter(tab => tab.key !== '/settings/organization/plans') | ||
|
|
||
| // Audit logs - visible only to super_admins | ||
| const needsAuditLogs = organizationStore.hasPermissionsInRole(organizationStore.currentRole, ['super_admin']) | ||
| const hasAuditLogs = organizationTabs.value.find(tab => tab.key === '/settings/organization/audit-logs') | ||
| if (needsAuditLogs && !hasAuditLogs) { | ||
| const base = baseOrgTabs.find(t => t.key === '/settings/organization/audit-logs') | ||
| if (base) | ||
| organizationTabs.value.push({ ...base }) | ||
| } | ||
| if (!needsAuditLogs && hasAuditLogs) | ||
| organizationTabs.value = organizationTabs.value.filter(tab => tab.key !== '/settings/organization/audit-logs') | ||
|
|
||
| // Security - visible only to super_admins | ||
| const needsSecurity = organizationStore.hasPermissionsInRole(organizationStore.currentRole, ['super_admin']) | ||
| const hasSecurity = organizationTabs.value.find(tab => tab.key === '/settings/organization/security') | ||
| if (needsSecurity && !hasSecurity) { | ||
| const base = baseOrgTabs.find(t => t.key === '/settings/organization/security') | ||
| if (base) | ||
| organizationTabs.value.push({ ...base }) | ||
| } | ||
| if (!needsSecurity && hasSecurity) | ||
| organizationTabs.value = organizationTabs.value.filter(tab => tab.key !== '/settings/organization/security') | ||
|
|
||
| if (!Capacitor.isNativePlatform() | ||
| && organizationStore.hasPermissionsInRole(organizationStore.currentRole, ['super_admin']) | ||
| && !organizationTabs.value.find(tab => tab.key === '/billing')) { | ||
| // Billing tab - always show on web, with different behavior for non-admins | ||
| if (!Capacitor.isNativePlatform() && !organizationTabs.value.find(tab => tab.key === '/billing')) { | ||
| organizationTabs.value.push({ | ||
| label: 'billing', | ||
| icon: IconBilling, | ||
| key: '/billing', | ||
| onClick: () => openPortal(organizationStore.currentOrganization?.gid ?? '', t), | ||
| onClick: () => { | ||
| // Check permissions at click time to handle role changes | ||
| if (organizationStore.hasPermissionsInRole(organizationStore.currentRole, ['super_admin'])) { | ||
| openPortal(organizationStore.currentOrganization?.gid ?? '', t) | ||
| } | ||
| else { | ||
| showBillingModal.value = true | ||
| } | ||
| }, | ||
| }) | ||
| } |
There was a problem hiding this comment.
The watchEffect adds the billing tab every time it runs if the tab doesn't exist. However, if organizationTabs.value is reset or modified elsewhere, this could lead to duplicate billing tabs being added. While the check '!organizationTabs.value.find(tab => tab.key === '/billing')' prevents immediate duplicates, consider tracking whether the tab has been added or using a computed property for tabs instead of watchEffect to avoid potential edge cases.
| try { | ||
| const members = await organizationStore.getMembers() | ||
| admins.value = members | ||
| .filter(m => m.role === 'super_admin' || m.role === 'admin') |
There was a problem hiding this comment.
The filtering includes both 'super_admin' and 'admin' roles, but all permission checks throughout the codebase (including in settings.vue) only check for 'super_admin'. This inconsistency means users with the 'admin' role will be shown in the modal as contacts, but they also cannot access these features. Either remove 'admin' from this filter or update the permission checks to allow 'admin' role as well.
| .filter(m => m.role === 'super_admin' || m.role === 'admin') | |
| .filter(m => m.role === 'super_admin') |
| <RouterView v-else class="w-full" /> | ||
| </div> | ||
| <!-- Admin-only modal for admin-only routes --> | ||
| <AdminOnlyModal v-if="showAdminOnlyModal" /> |
There was a problem hiding this comment.
The modal for admin-only routes (line 151) has no click handler, meaning users cannot dismiss it and are stuck on the page. While the billing modal (line 153) has a click handler, route-based modals should either redirect users back to an accessible route or provide a way to dismiss the modal. Consider adding navigation back to '/settings/organization' or another accessible route when the modal is clicked.
| '/settings/organization/plans', | ||
| '/settings/organization/credits', | ||
| '/settings/organization/audit-logs', | ||
| '/settings/organization/auditlogs', |
There was a problem hiding this comment.
The array contains both '/settings/organization/audit-logs' and '/settings/organization/auditlogs' as separate entries. This duplication is unnecessary - only one route should be needed. Check which route format is actually used in the router configuration and remove the duplicate.
| '/settings/organization/auditlogs', |
| <div class="flex absolute inset-0 z-10 flex-col justify-center items-center bg-white/60 dark:bg-gray-900/60"> | ||
| <div class="p-8 text-center bg-white rounded-xl border shadow-xl dark:bg-gray-800 border-blue-200 dark:border-blue-700 max-w-md"> | ||
| <div class="flex justify-center mb-4"> | ||
| <div class="flex justify-center items-center w-16 h-16 bg-blue-100 rounded-full dark:bg-blue-900/30"> | ||
| <svg class="w-8 h-8 text-blue-500" viewBox="0 0 20 20" fill="currentColor"> | ||
| <path | ||
| fill-rule="evenodd" | ||
| d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" | ||
| clip-rule="evenodd" | ||
| /> | ||
| </svg> | ||
| </div> | ||
| </div> | ||
| <h2 class="mb-2 text-2xl font-bold text-gray-900 dark:text-white"> | ||
| {{ t('admin-only-access') }} | ||
| </h2> | ||
| <p class="mb-4 text-gray-600 dark:text-gray-400"> | ||
| {{ t('admin-only-billing-description') }} | ||
| </p> | ||
| <div v-if="isLoading" class="flex justify-center py-2"> | ||
| <div class="w-6 h-6 rounded-full border-2 border-blue-500 border-t-transparent animate-spin" /> | ||
| </div> | ||
| <div v-else-if="admins.length > 0" class="mt-4 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg"> | ||
| <p class="mb-2 text-sm font-medium text-gray-700 dark:text-gray-300"> | ||
| {{ t('contact-your-admin') }}: | ||
| </p> | ||
| <div class="flex flex-wrap gap-2 justify-center"> | ||
| <div | ||
| v-for="admin in admins" | ||
| :key="admin.email" | ||
| class="flex items-center gap-2 px-3 py-1.5 bg-white dark:bg-gray-800 rounded-full border border-gray-200 dark:border-gray-600" | ||
| > | ||
| <img | ||
| v-if="admin.image_url" | ||
| :src="admin.image_url" | ||
| :alt="admin.email" | ||
| class="w-5 h-5 rounded-full" | ||
| > | ||
| <IconUserCircle v-else class="w-5 h-5 text-gray-400" /> | ||
| <span class="text-sm text-gray-700 dark:text-gray-300">{{ admin.email }}</span> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> |
There was a problem hiding this comment.
The modal lacks critical accessibility features: no focus trap to prevent tabbing out of the modal, no keyboard handler for ESC key to dismiss, no ARIA role="dialog", no aria-modal="true", and no aria-labelledby pointing to the heading. These features are essential for screen reader users and keyboard navigation. Consider wrapping the modal in a focus trap and adding appropriate ARIA attributes.
| <script setup lang="ts"> | ||
| import { onMounted, ref } from 'vue' | ||
| import { useI18n } from 'vue-i18n' | ||
| import IconUserCircle from '~icons/heroicons/user-circle' | ||
| import { useOrganizationStore } from '~/stores/organization' | ||
|
|
||
| const { t } = useI18n() | ||
| const organizationStore = useOrganizationStore() | ||
|
|
||
| const admins = ref<{ email: string, image_url: string }[]>([]) | ||
| const isLoading = ref(true) | ||
|
|
||
| onMounted(async () => { | ||
| try { | ||
| const members = await organizationStore.getMembers() | ||
| admins.value = members | ||
| .filter(m => m.role === 'super_admin' || m.role === 'admin') | ||
| .map(m => ({ email: m.email, image_url: m.image_url })) | ||
| } | ||
| catch (e) { | ||
| console.error('Failed to fetch admins:', e) | ||
| } | ||
| finally { | ||
| isLoading.value = false | ||
| } | ||
| }) | ||
| </script> | ||
|
|
||
| <template> | ||
| <div class="flex absolute inset-0 z-10 flex-col justify-center items-center bg-white/60 dark:bg-gray-900/60"> | ||
| <div class="p-8 text-center bg-white rounded-xl border shadow-xl dark:bg-gray-800 border-blue-200 dark:border-blue-700 max-w-md"> | ||
| <div class="flex justify-center mb-4"> | ||
| <div class="flex justify-center items-center w-16 h-16 bg-blue-100 rounded-full dark:bg-blue-900/30"> | ||
| <svg class="w-8 h-8 text-blue-500" viewBox="0 0 20 20" fill="currentColor"> | ||
| <path | ||
| fill-rule="evenodd" | ||
| d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" | ||
| clip-rule="evenodd" | ||
| /> | ||
| </svg> | ||
| </div> | ||
| </div> | ||
| <h2 class="mb-2 text-2xl font-bold text-gray-900 dark:text-white"> | ||
| {{ t('admin-only-access') }} | ||
| </h2> | ||
| <p class="mb-4 text-gray-600 dark:text-gray-400"> | ||
| {{ t('admin-only-billing-description') }} | ||
| </p> | ||
| <div v-if="isLoading" class="flex justify-center py-2"> | ||
| <div class="w-6 h-6 rounded-full border-2 border-blue-500 border-t-transparent animate-spin" /> | ||
| </div> | ||
| <div v-else-if="admins.length > 0" class="mt-4 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg"> | ||
| <p class="mb-2 text-sm font-medium text-gray-700 dark:text-gray-300"> | ||
| {{ t('contact-your-admin') }}: | ||
| </p> | ||
| <div class="flex flex-wrap gap-2 justify-center"> | ||
| <div | ||
| v-for="admin in admins" | ||
| :key="admin.email" | ||
| class="flex items-center gap-2 px-3 py-1.5 bg-white dark:bg-gray-800 rounded-full border border-gray-200 dark:border-gray-600" | ||
| > | ||
| <img | ||
| v-if="admin.image_url" | ||
| :src="admin.image_url" | ||
| :alt="admin.email" | ||
| class="w-5 h-5 rounded-full" | ||
| > | ||
| <IconUserCircle v-else class="w-5 h-5 text-gray-400" /> | ||
| <span class="text-sm text-gray-700 dark:text-gray-300">{{ admin.email }}</span> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </template> |
There was a problem hiding this comment.
The new AdminOnlyModal component and the modal triggering behavior in settings.vue lack test coverage. Since the repository has Playwright E2E tests for frontend functionality, consider adding tests that verify: 1) Non-admin users can see all tabs, 2) Clicking admin-only tabs shows the modal with admin contact information, 3) The modal can be dismissed for billing tabs, 4) Non-admin users navigating directly to admin-only routes see the modal.
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 21e53e9b20
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| onMounted(async () => { | ||
| try { | ||
| const members = await organizationStore.getMembers() | ||
| admins.value = members |
There was a problem hiding this comment.
Re-fetch admins after org is loaded
The modal only calls organizationStore.getMembers() once in onMounted. On a hard refresh into an admin-only settings route, currentOrganization is often still unset when this runs, and getMembers() returns an empty list in that case. Because there’s no watch or retry when the org becomes available, the modal can permanently show no admin contacts even after the org finishes loading. Consider watching currentOrganization (or a fetch completion flag) and reloading admins when it becomes defined.
Useful? React with 👍 / 👎.



Summary
Non-admin users can now see all billing and subscription tabs instead of having them hidden. When clicked, a modal appears showing which admins they need to contact. This improves UX by making features discoverable while preventing unauthorized access with clear guidance.
Test plan
Checklist
bun run lint🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Localization
✏️ Tip: You can customize this high-level summary in your review settings.