Skip to content

refactor: extract shared hasPermission helper (MODSetter/SurfSense#1366)#1428

Merged
MODSetter merged 1 commit into
MODSetter:devfrom
guangyang1206:fix/extract-shared-haspermission-helper-1366
May 23, 2026
Merged

refactor: extract shared hasPermission helper (MODSetter/SurfSense#1366)#1428
MODSetter merged 1 commit into
MODSetter:devfrom
guangyang1206:fix/extract-shared-haspermission-helper-1366

Conversation

@guangyang1206
Copy link
Copy Markdown
Contributor

@guangyang1206 guangyang1206 commented May 22, 2026

  • Add canPerform() helper function to members-query.atoms.ts
  • Add usePermissionGate() hook for convenience
  • Update team-content.tsx to use canPerform()
  • Update roles-manager.tsx to use canPerform()
  • Eliminates duplicated permission check logic
  • Centralizes permission policy in one location

Fixes #1366

Description

Problem

The permission check predicate is byte-identical in two settings/team components:

  • surfsense_web/app/dashboard/[search_space_id]/team/team-content.tsx:128
  • surfsense_web/components/settings/roles-manager.tsx:259

Both get access from useAtomValue(myAccessAtom). Other components that need the same check also read myAccessAtom and would benefit from the shared helper.

If the owner-bypass semantics ever change, every duplicated copy must be updated in lockstep.

Solution

This PR extracts the duplicated permission check logic into a shared helper function canPerform() added to surfsense_web/atoms/members/members-query.atoms.ts, alongside myAccessAtom.

Changes:

  1. Add canPerform() helper function in members-query.atoms.ts:

    • Pure function that takes access and permission as arguments
    • Returns boolean indicating if the user has the permission
    • Handles null/undefined access, is_owner bypass, and permission array lookup
  2. Add usePermissionGate() hook (optional convenience wrapper):

    • Reads myAccessAtom internally
    • Returns (perm: string) => boolean
    • Useful for components that want to avoid calling useAtomValue(myAccessAtom) separately
  3. Update team-content.tsx:

    • Import canPerform from atoms file
    • Replace inline useCallback with call to canPerform(access, permission)
  4. Update roles-manager.tsx:

    • Import canPerform from atoms file
    • Replace inline useCallback with call to canPerform(access, permission)

Benefits

  • Locality: Permission policy lives in one place. Owner-bypass logic, future role bypasses, and null-handling rules are tested once.
  • Smaller test surface: One pure function easy to unit-test instead of two useCallbacks embedded in 800-line components.
  • Reuse: Other components currently re-checking access.is_owner or access.permissions?.includes(...) inline can adopt the helper in follow-up PRs.

Testing

  1. Navigate to team settings page - verify permissions work correctly
  2. Navigate to roles manager - verify permissions work correctly
  3. Test with different user roles (owner, admin, member) - verify access control behaves identically to before
  4. Verify no TypeScript errors: npm run lint and npm run type-check

Related Issues

Fixes #1366


PR Link

main...guangyang1206:SurfSense:fix/extract-shared-haspermission-helper-1366

Branch

guangyang1206:fix/extract-shared-haspermission-helper-1366

High-level PR Summary

This PR extracts duplicated permission checking logic into a shared canPerform() helper function and usePermissionGate() hook. The helper centralizes the permission validation logic (including owner bypass and null handling) that was previously duplicated in team-content.tsx and roles-manager.tsx, making the permission policy easier to maintain and test in one location.

⏱️ Estimated Review Time: 5-15 minutes

💡 Review Order Suggestion
Order File Path
1 surfsense_web/atoms/members/members-query.atoms.ts
2 surfsense_web/app/dashboard/[search_space_id]/team/team-content.tsx
3 surfsense_web/components/settings/roles-manager.tsx
⚠️ Inconsistent Changes Detected
File Path Warning
surfsense_web/components/settings/roles-manager.tsx The file contains leftover duplicate code - the old permission check implementation (lines 263-267) was not removed after adding the new canPerform() implementation (lines 259-261), creating duplicate useCallback definitions

Need help? Join our Discord

Summary by CodeRabbit

  • Refactor
    • Consolidated permission-checking logic into reusable utilities for consistency across the application.

Review Change Stack

- Add canPerform() helper function to members-query.atoms.ts
- Add usePermissionGate() hook for convenience
- Update team-content.tsx to use canPerform()
- Update roles-manager.tsx to use canPerform()
- Eliminates duplicated permission check logic
- Centralizes permission policy in one location

Fixes MODSetter#1366
@vercel
Copy link
Copy Markdown

vercel Bot commented May 22, 2026

@guangyang1206 is attempting to deploy a commit to the Rohan Verma's projects Team on Vercel.

A member of the Team first needs to authorize it.

@guangyang1206 guangyang1206 changed the base branch from main to dev May 22, 2026 09:51
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 22, 2026

📝 Walkthrough

Walkthrough

This PR consolidates duplicate permission-checking logic from two components into a shared helper. A reusable canPerform function and usePermissionGate hook are added to the atoms module, then both team-content.tsx and roles-manager.tsx are updated to delegate their permission checks to the shared implementation.

Changes

Permission-Check Consolidation

Layer / File(s) Summary
Shared permission-check helpers in atoms module
surfsense_web/atoms/members/members-query.atoms.ts
New canPerform(access, permission) pure helper that returns false for null/undefined access, true for owner access, otherwise checks permission inclusion. New usePermissionGate(permission) hook wrapper that reads myAccessAtom and delegates to canPerform.
Team-content component delegates to shared helper
surfsense_web/app/dashboard/[search_space_id]/team/team-content.tsx
Import canPerform and replace inline permission-check callback with a call to canPerform(access, permission), preserving the memoization dependency on access.
Roles-manager component delegates to shared helper
surfsense_web/components/settings/roles-manager.tsx
Import canPerform and replace inline permission-check callback with a call to canPerform(access, permission), preserving the memoization dependency on access.

🎯 2 (Simple) | ⏱️ ~8 minutes

🐰 Two callbacks so alike, they were twins,
Now merged in one module where clean code wins,
Owner or permissions, the helper now decides,
No more duplication in two files side by side.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main refactoring: extracting a shared hasPermission helper to replace duplicated code.
Linked Issues check ✅ Passed The PR successfully implements Option A from issue #1366: adds canPerform() pure helper and usePermissionGate() hook, replaces duplicated permission checks in team-content.tsx and roles-manager.tsx, and centralizes permission logic as required.
Out of Scope Changes check ✅ Passed All changes are directly in scope: two components updated to use the new canPerform helper, and the helper was added to the atoms module alongside myAccessAtom as specified.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

Copy link
Copy Markdown

@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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
surfsense_web/components/settings/roles-manager.tsx (1)

259-269: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Syntax error: duplicate callback body — old code was not removed.

The refactor left the old inline permission check in place (lines 263-269) alongside the new canPerform call (lines 260-262). This creates invalid JavaScript syntax and will fail to compile.

🐛 Proposed fix — remove the leftover code
 	const hasPermission = useCallback(
 		(permission: string) => canPerform(access, permission),
 		[access]
 	);
-		(permission: string) => {
-			if (!access) return false;
-			if (access.is_owner) return true;
-			return access.permissions?.includes(permission) ?? false;
-		},
-		[access]
-	);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@surfsense_web/components/settings/roles-manager.tsx` around lines 259 - 269,
The file contains a duplicated callback body: the useCallback assigned to
hasPermission should call canPerform(access, permission) but the old inline
function (the anonymous permission check using access.is_owner and
access.permissions) was left below, causing a syntax error; remove the leftover
anonymous function block (the second function and its trailing dependency array)
and keep the single useCallback definition for hasPermission that references
canPerform and the [access] dependency so hasPermission =
useCallback((permission: string) => canPerform(access, permission), [access]).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@surfsense_web/app/dashboard/`[search_space_id]/team/team-content.tsx:
- Line 34: The import line importing membersAtom, myAccessAtom, and canPerform
in team-content.tsx violates Biome's organizeImports alphabetical rule; reorder
the named imports inside the curly braces alphabetically (canPerform,
membersAtom, myAccessAtom) so the import becomes sorted and re-run the pipeline
to ensure the Biome organizeImports check passes.

In `@surfsense_web/atoms/members/members-query.atoms.ts`:
- Around line 73-76: The hook usePermissionGate currently calls
useAtomValue(myAccessAtom) without importing useAtomValue and treats the atom as
returning raw access; import useAtomValue from 'jotai', then call const { data:
access } = useAtomValue(myAccessAtom) (or similar destructuring) and pass that
access into canPerform(access, permission) so usePermissionGate uses the atom's
.data field rather than the query wrapper.

---

Outside diff comments:
In `@surfsense_web/components/settings/roles-manager.tsx`:
- Around line 259-269: The file contains a duplicated callback body: the
useCallback assigned to hasPermission should call canPerform(access, permission)
but the old inline function (the anonymous permission check using
access.is_owner and access.permissions) was left below, causing a syntax error;
remove the leftover anonymous function block (the second function and its
trailing dependency array) and keep the single useCallback definition for
hasPermission that references canPerform and the [access] dependency so
hasPermission = useCallback((permission: string) => canPerform(access,
permission), [access]).
🪄 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: a28d8ad4-ffeb-433e-9401-2111800c164e

📥 Commits

Reviewing files that changed from the base of the PR and between af35106 and a66d65a.

📒 Files selected for processing (3)
  • surfsense_web/app/dashboard/[search_space_id]/team/team-content.tsx
  • surfsense_web/atoms/members/members-query.atoms.ts
  • surfsense_web/components/settings/roles-manager.tsx

updateMemberMutationAtom,
} from "@/atoms/members/members-mutation.atoms";
import { membersAtom, myAccessAtom } from "@/atoms/members/members-query.atoms";
import { membersAtom, myAccessAtom, canPerform } from "@/atoms/members/members-query.atoms";
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix import ordering to resolve pipeline failure.

The Biome organizeImports check failed. Named imports should be sorted alphabetically.

Proposed fix
-import { membersAtom, myAccessAtom, canPerform } from "`@/atoms/members/members-query.atoms`";
+import { canPerform, membersAtom, myAccessAtom } from "`@/atoms/members/members-query.atoms`";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { membersAtom, myAccessAtom, canPerform } from "@/atoms/members/members-query.atoms";
import { canPerform, membersAtom, myAccessAtom } from "`@/atoms/members/members-query.atoms`";
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@surfsense_web/app/dashboard/`[search_space_id]/team/team-content.tsx at line
34, The import line importing membersAtom, myAccessAtom, and canPerform in
team-content.tsx violates Biome's organizeImports alphabetical rule; reorder the
named imports inside the curly braces alphabetically (canPerform, membersAtom,
myAccessAtom) so the import becomes sorted and re-run the pipeline to ensure the
Biome organizeImports check passes.

Comment on lines +73 to +76
export function usePermissionGate(permission: string): boolean {
const access = useAtomValue(myAccessAtom);
return canPerform(access, permission);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Missing import and incorrect data access will break usePermissionGate.

  1. useAtomValue is used but never imported from jotai.
  2. myAccessAtom is created via atomWithQuery, so useAtomValue(myAccessAtom) returns the query result object { data, isLoading, ... }, not the access data directly. You need to destructure .data.
🐛 Proposed fix

Add the import at the top of the file:

 import { atomWithQuery } from "jotai-tanstack-query";
+import { useAtomValue } from "jotai";
 import { activeSearchSpaceIdAtom } from "`@/atoms/search-spaces/search-space-query.atoms`";

Then fix the hook implementation:

 export function usePermissionGate(permission: string): boolean {
-	const access = useAtomValue(myAccessAtom);
-	return canPerform(access, permission);
+	const { data: access } = useAtomValue(myAccessAtom);
+	return canPerform(access ?? null, permission);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function usePermissionGate(permission: string): boolean {
const access = useAtomValue(myAccessAtom);
return canPerform(access, permission);
}
export function usePermissionGate(permission: string): boolean {
const { data: access } = useAtomValue(myAccessAtom);
return canPerform(access ?? null, permission);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@surfsense_web/atoms/members/members-query.atoms.ts` around lines 73 - 76, The
hook usePermissionGate currently calls useAtomValue(myAccessAtom) without
importing useAtomValue and treats the atom as returning raw access; import
useAtomValue from 'jotai', then call const { data: access } =
useAtomValue(myAccessAtom) (or similar destructuring) and pass that access into
canPerform(access, permission) so usePermissionGate uses the atom's .data field
rather than the query wrapper.

@MODSetter MODSetter merged commit ee87747 into MODSetter:dev May 23, 2026
4 of 9 checks passed
MODSetter pushed a commit that referenced this pull request May 25, 2026
PR #1428 (issue #1366) extracted the inline `hasPermission` callback into
a shared `canPerform` helper but left the original arrow-function body,
its dependency array, and trailing `)` behind after the new
`useCallback` block. The result was a syntactically invalid statement
that broke `pnpm build` on the `dev` branch and is now blocking every
E2E job in the PR queue.

Delete the orphaned lines so the file parses again. No behavior change —
the working `useCallback(canPerform(access, permission))` already
supplies the same predicate the duplicated body did.
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.

Extract shared hasPermission helper instead of duplicating the predicate in two components

2 participants