Skip to content

Conversation

sean-brydon
Copy link
Member

@sean-brydon sean-brydon commented Jul 18, 2025

Seed your database by "yarn seed-pbac"

Look at the roles created in orgs/teams roles and permissions in settings.

Create new roles/edit old ones to remove all permission from event type - assign it to a user.
Impersonate the user and try to CRUD an event type.
Tick on permissions in the role and keep testing the event type.

Impersonate a team without pbac enabled and ensure everything works as expected

Copy link
Contributor

coderabbitai bot commented Jul 18, 2025

Walkthrough

Refactors event-type viewing and creation to use PBAC and extracts membership/event-group logic into new classes and utilities. Adds EventGroupBuilder, TeamAccessUseCase, ProfilePermissionProcessor, EventTypeGroupFilter, permission/transform utilities, and tests. Replaces owner-only procedures with createEventPbacProcedure for several event-type mutations, updates create/getUserEventGroups handlers to call PBAC and the new builders, and wires permission-aware data into UI wrappers and hooks.

Possibly related PRs

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/event-type-pbac

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

github-actions bot commented Jul 18, 2025

Hey there and thank you for opening this pull request! 👋🏼

We require pull request titles to follow the Conventional Commits specification and it looks like your proposed title needs to be adjusted.

Details:

No release type found in pull request title "[DO NOT MERGE] feat: event type pbac". Add a prefix to indicate what kind of release this pull request corresponds to. For reference, see https://www.conventionalcommits.org/

Available types:
 - feat: A new feature
 - fix: A bug fix
 - docs: Documentation only changes
 - style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
 - refactor: A code change that neither fixes a bug nor adds a feature
 - perf: A code change that improves performance
 - test: Adding missing tests or correcting existing tests
 - build: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
 - ci: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)
 - chore: Other changes that don't modify src or test files
 - revert: Reverts a previous commit

@github-actions github-actions bot added the ❗️ migrations contains migration files label Jul 18, 2025
@keithwillcode keithwillcode added consumer core area: core, team members only labels Jul 18, 2025
Copy link

delve-auditor bot commented Jul 18, 2025

No security or compliance issues detected. Reviewed everything up to 1bfed57.

Security Overview
  • 🔎 Scanned files: 29 changed file(s)
Detected Code Changes
Change Type Relevant files
Enhancement ► attributes-list-view.tsx
    Add permission checks for organization attributes
► resource-permissions.ts
    Implement PBAC resource permissions logic
► organizations/update.handler.ts
    Update organization permissions handling
Configuration changes ► permission-registry.ts
    Add attribute permissions configuration
► common.json
    Add new permission translations
Refactor ► page.tsx files
    Update organization settings pages with permission checks
► teamAccessUseCase.ts
    Extract team access logic

Reply to this PR with @delve-auditor followed by a description of what change you want and we'll auto-submit a change to this PR to implement it.

@sean-brydon sean-brydon changed the title Feat/event type pbac [DO NOT MERGE] feat: event type pbac Jul 18, 2025
@sean-brydon sean-brydon force-pushed the feat/event-type-pbac branch from a1d62f0 to a4a0da4 Compare July 18, 2025 09:46
Copy link
Contributor

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

🧹 Nitpick comments (2)
packages/trpc/server/routers/viewer/pbac/_router.tsx (1)

18-47: LGTM! Enhanced permission string validation handles nested resources correctly.

The new validation logic properly addresses complex resource names using a longest-match strategy. The special case handling for ._resource and the sorted resource matching ensure accurate parsing of permission strings like "organization.attributes.read".

Consider caching the sorted resources array to avoid sorting on every validation:

+// Cache sorted resources outside the validation function
+const sortedResourceValues = Object.values(Resource).sort((a, b) => b.length - a.length);
+
 const permissionStringSchema = z.custom<PermissionString>((val) => {
   if (typeof val !== "string") return false;

   // Handle special case for _resource
   if (val.endsWith("._resource")) {
     return true;
   }

-  // Find the longest matching resource from the end
-  const resourceValues = Object.values(Resource);
   let matchedResource: string | null = null;
   let remainingAction: string | null = null;

-  // Sort resources by length (longest first) to match the most specific resource
-  const sortedResources = resourceValues.sort((a, b) => b.length - a.length);
-
-  for (const resource of sortedResources) {
+  for (const resource of sortedResourceValues) {
packages/features/pbac/infrastructure/repositories/RoleRepository.ts (1)

11-38: Excellent consistency with router validation logic.

The parsePermissionString helper function implements the same longest-match strategy as the router validation, ensuring consistent handling of complex permission strings throughout the system. The documentation is clear and the special case handling for ._resource is appropriate.

Consider caching the sorted resources array for performance, similar to the suggestion for the router:

+// Cache sorted resources outside the function
+const sortedResourceValues = Object.values(Resource).sort((a, b) => b.length - a.length);
+
 function parsePermissionString(permissionString: string): { resource: string; action: string } {
   // Handle special case for _resource
   if (permissionString.endsWith("._resource")) {
     const resource = permissionString.substring(0, permissionString.length - 10);
     return { resource, action: "_resource" };
   }

-  // Find the longest matching resource from the end
-  const resourceValues = Object.values(Resource);
-
-  // Sort resources by length (longest first) to match the most specific resource
-  const sortedResources = resourceValues.sort((a, b) => b.length - a.length);
-
-  for (const resource of sortedResources) {
+  for (const resource of sortedResourceValues) {
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7526c8c and 35286f1.

📒 Files selected for processing (2)
  • packages/features/pbac/infrastructure/repositories/RoleRepository.ts (2 hunks)
  • packages/trpc/server/routers/viewer/pbac/_router.tsx (1 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*Repository.ts

📄 CodeRabbit Inference Engine (.cursor/rules/review.mdc)

Repository files must include Repository suffix, prefix with technology if applicable (e.g., PrismaAppRepository.ts), and use PascalCase matching the exported class

Files:

  • packages/features/pbac/infrastructure/repositories/RoleRepository.ts
**/*.ts

📄 CodeRabbit Inference Engine (.cursor/rules/review.mdc)

**/*.ts: For Prisma queries, only select data you need; never use include, always use select
Ensure the credential.key field is never returned from tRPC endpoints or APIs

Files:

  • packages/features/pbac/infrastructure/repositories/RoleRepository.ts
**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (.cursor/rules/review.mdc)

Flag excessive Day.js use in performance-critical code; prefer native Date or Day.js .utc() in hot paths like loops

Files:

  • packages/features/pbac/infrastructure/repositories/RoleRepository.ts
  • packages/trpc/server/routers/viewer/pbac/_router.tsx
**/*.tsx

📄 CodeRabbit Inference Engine (.cursor/rules/review.mdc)

Always use t() for text localization in frontend code; direct text embedding should trigger a warning

Files:

  • packages/trpc/server/routers/viewer/pbac/_router.tsx
🧠 Learnings (3)
📓 Common learnings
Learnt from: sean-brydon
PR: calcom/cal.com#22618
File: packages/trpc/server/routers/viewer/eventTypes/utils/transformUtils.ts:113-113
Timestamp: 2025-08-05T07:42:06.335Z
Learning: In Cal.com's getUserEventGroups handler refactor (PR #22618), the membershipCount field for team event groups is intentionally set to 0 in the new createTeamEventGroup function, as confirmed by sean-brydon (PR author). This preserves the same behavior as the old implementation that was being refactored, maintaining backward compatibility. While other parts of the codebase may use actual member counts, this specific implementation maintains the previous behavior.
📚 Learning: applies to **/*repository.ts : repository files must include `repository` suffix, prefix with techno...
Learnt from: CR
PR: calcom/cal.com#0
File: .cursor/rules/review.mdc:0-0
Timestamp: 2025-07-28T11:50:23.946Z
Learning: Applies to **/*Repository.ts : Repository files must include `Repository` suffix, prefix with technology if applicable (e.g., `PrismaAppRepository.ts`), and use PascalCase matching the exported class

Applied to files:

  • packages/features/pbac/infrastructure/repositories/RoleRepository.ts
📚 Learning: in signup-view.tsx, when checking if redirecturl contains certain strings, using explicit && checks ...
Learnt from: Anshumancanrock
PR: calcom/cal.com#22570
File: apps/web/modules/signup-view.tsx:253-253
Timestamp: 2025-07-21T21:33:23.371Z
Learning: In signup-view.tsx, when checking if redirectUrl contains certain strings, using explicit && checks (redirectUrl && redirectUrl.includes()) is preferred over optional chaining (redirectUrl?.includes()) to ensure the result is always a boolean rather than potentially undefined. This approach provides cleaner boolean contracts for downstream conditional logic.

Applied to files:

  • packages/trpc/server/routers/viewer/pbac/_router.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Install dependencies / Yarn install & cache
🔇 Additional comments (1)
packages/features/pbac/infrastructure/repositories/RoleRepository.ts (1)

87-88: LGTM! Proper integration of enhanced permission parsing.

The usage of parsePermissionString in the create method correctly replaces the previous simple split approach, ensuring complex permission strings are properly parsed before database insertion.

Copy link
Contributor

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

🧹 Nitpick comments (1)
packages/trpc/server/routers/viewer/eventTypes/utils/EventTypeGroupFilter.ts (1)

32-44: Consider extracting permission mapping to improve maintainability.

The switch statement maps permission strings to permission fields, but this creates a tight coupling between the permission strings and the TeamPermissions interface structure.

Consider extracting this mapping to a separate utility function or configuration:

+const PERMISSION_MAPPING = {
+  "eventType.read": () => true,
+  "eventType.create": (permissions: TeamPermissions) => permissions.canCreate,
+  "eventType.update": (permissions: TeamPermissions) => permissions.canEdit,
+  "eventType.delete": (permissions: TeamPermissions) => permissions.canDelete,
+} as const;

-      switch (permission) {
-        case "eventType.read":
-          // All groups with permissions can read
-          return true;
-        case "eventType.create":
-          return permissions.canCreate;
-        case "eventType.update":
-          return permissions.canEdit;
-        case "eventType.delete":
-          return permissions.canDelete;
-        default:
-          return false;
-      }
+      const permissionCheck = PERMISSION_MAPPING[permission];
+      return permissionCheck ? permissionCheck(permissions) : false;
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 35286f1 and 1133758.

📒 Files selected for processing (3)
  • packages/trpc/server/routers/viewer/eventTypes/getUserEventGroups.handler.ts (2 hunks)
  • packages/trpc/server/routers/viewer/eventTypes/utils/EventTypeGroupFilter.test.ts (1 hunks)
  • packages/trpc/server/routers/viewer/eventTypes/utils/EventTypeGroupFilter.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.ts

📄 CodeRabbit Inference Engine (.cursor/rules/review.mdc)

**/*.ts: For Prisma queries, only select data you need; never use include, always use select
Ensure the credential.key field is never returned from tRPC endpoints or APIs

Files:

  • packages/trpc/server/routers/viewer/eventTypes/utils/EventTypeGroupFilter.test.ts
  • packages/trpc/server/routers/viewer/eventTypes/getUserEventGroups.handler.ts
  • packages/trpc/server/routers/viewer/eventTypes/utils/EventTypeGroupFilter.ts
**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (.cursor/rules/review.mdc)

Flag excessive Day.js use in performance-critical code; prefer native Date or Day.js .utc() in hot paths like loops

Files:

  • packages/trpc/server/routers/viewer/eventTypes/utils/EventTypeGroupFilter.test.ts
  • packages/trpc/server/routers/viewer/eventTypes/getUserEventGroups.handler.ts
  • packages/trpc/server/routers/viewer/eventTypes/utils/EventTypeGroupFilter.ts
🧠 Learnings (4)
📓 Common learnings
Learnt from: sean-brydon
PR: calcom/cal.com#22618
File: packages/trpc/server/routers/viewer/eventTypes/utils/transformUtils.ts:113-113
Timestamp: 2025-08-05T07:42:06.335Z
Learning: In Cal.com's getUserEventGroups handler refactor (PR #22618), the membershipCount field for team event groups is intentionally set to 0 in the new createTeamEventGroup function, as confirmed by sean-brydon (PR author). This preserves the same behavior as the old implementation that was being refactored, maintaining backward compatibility. While other parts of the codebase may use actual member counts, this specific implementation maintains the previous behavior.
📚 Learning: in cal.com's getusereventgroups handler refactor (pr #22618), the membershipcount field for team eve...
Learnt from: sean-brydon
PR: calcom/cal.com#22618
File: packages/trpc/server/routers/viewer/eventTypes/utils/transformUtils.ts:113-113
Timestamp: 2025-08-05T07:42:06.335Z
Learning: In Cal.com's getUserEventGroups handler refactor (PR #22618), the membershipCount field for team event groups is intentionally set to 0 in the new createTeamEventGroup function, as confirmed by sean-brydon (PR author). This preserves the same behavior as the old implementation that was being refactored, maintaining backward compatibility. While other parts of the codebase may use actual member counts, this specific implementation maintains the previous behavior.

Applied to files:

  • packages/trpc/server/routers/viewer/eventTypes/utils/EventTypeGroupFilter.test.ts
  • packages/trpc/server/routers/viewer/eventTypes/getUserEventGroups.handler.ts
  • packages/trpc/server/routers/viewer/eventTypes/utils/EventTypeGroupFilter.ts
📚 Learning: in cal.com's event type system, the membershipcount field for team event groups is intentionally set...
Learnt from: sean-brydon
PR: calcom/cal.com#22618
File: packages/trpc/server/routers/viewer/eventTypes/utils/transformUtils.ts:113-113
Timestamp: 2025-08-05T07:42:06.335Z
Learning: In Cal.com's event type system, the membershipCount field for team event groups is intentionally set to 0, as confirmed by sean-brydon (PR author). This behavior was preserved during the refactor from the old getUserEventGroups.handler.ts implementation to the new createTeamEventGroup function in transformUtils.ts. This is not a bug but the intended behavior that maintains consistency with the previous implementation.

Applied to files:

  • packages/trpc/server/routers/viewer/eventTypes/utils/EventTypeGroupFilter.test.ts
  • packages/trpc/server/routers/viewer/eventTypes/getUserEventGroups.handler.ts
  • packages/trpc/server/routers/viewer/eventTypes/utils/EventTypeGroupFilter.ts
📚 Learning: in cal.com's event type system, the membershipcount field for team event groups is intentionally har...
Learnt from: sean-brydon
PR: calcom/cal.com#22618
File: packages/trpc/server/routers/viewer/eventTypes/utils/transformUtils.ts:113-113
Timestamp: 2025-08-05T07:42:06.335Z
Learning: In Cal.com's event type system, the membershipCount field for team event groups is intentionally hard-coded to 0, as confirmed by sean-brydon. This behavior was preserved during the refactor from the old getUserEventGroups.handler.ts implementation to the new createTeamEventGroup function in transformUtils.ts. This is not a bug but the intended behavior.

Applied to files:

  • packages/trpc/server/routers/viewer/eventTypes/utils/EventTypeGroupFilter.test.ts
  • packages/trpc/server/routers/viewer/eventTypes/getUserEventGroups.handler.ts
  • packages/trpc/server/routers/viewer/eventTypes/utils/EventTypeGroupFilter.ts
🧬 Code Graph Analysis (1)
packages/trpc/server/routers/viewer/eventTypes/utils/EventTypeGroupFilter.ts (1)
packages/trpc/server/routers/viewer/eventTypes/utils/transformUtils.ts (1)
  • EventTypeGroup (10-25)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Install dependencies / Yarn install & cache
🔇 Additional comments (10)
packages/trpc/server/routers/viewer/eventTypes/utils/EventTypeGroupFilter.test.ts (1)

60-102: LGTM! Comprehensive permission filtering test coverage.

The test suite thoroughly covers all permission scenarios and edge cases for the EventTypeGroupFilter.has() method, including proper handling of user groups (which always have permissions) and team groups (which depend on the permissions map).

packages/trpc/server/routers/viewer/eventTypes/utils/EventTypeGroupFilter.ts (3)

6-13: LGTM! Clean class structure with proper encapsulation.

The constructor properly initializes the private fields and the class maintains immutability by creating new instances in filtering methods.


20-48: LGTM! Solid permission filtering logic with clear user vs team group handling.

The has() method correctly:

  • Grants all permissions to user groups (teamId is null/undefined)
  • Uses the permissions map for team groups
  • Provides sensible defaults for unknown permissions
  • Handles the case where team permissions are not found in the map

119-124: LGTM! Clean factory function for fluent API.

The filterEvents factory function provides a clean entry point for the fluent filtering API.

packages/trpc/server/routers/viewer/eventTypes/getUserEventGroups.handler.ts (6)

10-13: LGTM! Clean import organization for new abstractions.

The imports are well-organized and bring in the necessary new classes for the refactored implementation.


38-43: LGTM! Good dependency injection pattern.

The dependencies object properly encapsulates the repository and use case dependencies, making the code more testable and modular.


45-52: LGTM! Clean delegation to EventGroupBuilder.

The handler properly delegates the complex event group building logic to the specialized EventGroupBuilder class, passing all necessary parameters.


54-56: LGTM! Proper application of permission filtering.

The use of EventTypeGroupFilter to filter by "eventType.read" permission is appropriate and aligns with the access control requirements.


58-60: LGTM! Clean profile processing delegation.

The handler properly uses the ProfilePermissionProcessor to handle profile permission logic, maintaining separation of concerns.


68-69: LGTM! Good backward compatibility consideration.

Re-exporting the compareMembership function maintains backward compatibility for existing code that may depend on importing it from this module.

sean-brydon and others added 2 commits August 8, 2025 09:39
…GroupFilter.test.ts

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Copy link
Contributor

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

♻️ Duplicate comments (1)
packages/trpc/server/routers/viewer/eventTypes/utils/EventTypeGroupFilter.test.ts (1)

163-166: Remove leftover diff markers and duplicate declaration; use the correct permission key.

You have committed diff artifacts here (lines starting with "-" and "+"). This causes the Biome parse errors and the duplicate result declaration flagged in static analysis. Also, the old permission "eventType.edit" must be replaced with the standardized "eventType.update".

Apply this fix:

-      const result = filterEvents(allGroups, mockPermissionsMap).has("eventType.edit").readOnly(false).get();
-      const result = filterEvents(allGroups, mockPermissionsMap).has("eventType.update").readOnly(false).get();
+      const result = filterEvents(allGroups, mockPermissionsMap)
+        .has("eventType.update")
+        .readOnly(false)
+        .get();

Run this quick check to ensure no other stray diff markers made it into source files:

#!/bin/bash
# Search for lines that start with a lone '+' or '-' followed by a space (common leftover from diffs)
rg -n '^[[:space:]]*[+-][[:space:]]+[^/*+-]' --glob '!**/node_modules/**'

Expected: no matches. If matches appear in this test file, remove the offending lines.

🧹 Nitpick comments (1)
packages/trpc/server/routers/viewer/eventTypes/utils/EventTypeGroupFilter.test.ts (1)

35-36: Align team mocks’ membershipCount with intended semantics (0).

Per the retrieved learnings for this PR, team event groups intentionally report membershipCount: 0 to preserve legacy behavior. While these tests don’t assert on membershipCount, aligning the mocks avoids confusion and reduces accidental reliance later.

   metadata: {
-      membershipCount: 5,
+      membershipCount: 0,
       readOnly: false,
   },
...
   metadata: {
-      membershipCount: 3,
+      membershipCount: 0,
       readOnly: true,
   },

Also applies to: 50-51

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1133758 and cfe27dd.

📒 Files selected for processing (1)
  • packages/trpc/server/routers/viewer/eventTypes/utils/EventTypeGroupFilter.test.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.ts

📄 CodeRabbit Inference Engine (.cursor/rules/review.mdc)

**/*.ts: For Prisma queries, only select data you need; never use include, always use select
Ensure the credential.key field is never returned from tRPC endpoints or APIs

Files:

  • packages/trpc/server/routers/viewer/eventTypes/utils/EventTypeGroupFilter.test.ts
**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (.cursor/rules/review.mdc)

Flag excessive Day.js use in performance-critical code; prefer native Date or Day.js .utc() in hot paths like loops

Files:

  • packages/trpc/server/routers/viewer/eventTypes/utils/EventTypeGroupFilter.test.ts
🧠 Learnings (6)
📓 Common learnings
Learnt from: sean-brydon
PR: calcom/cal.com#22618
File: packages/trpc/server/routers/viewer/eventTypes/utils/transformUtils.ts:113-113
Timestamp: 2025-08-05T07:42:06.335Z
Learning: In Cal.com's getUserEventGroups handler refactor (PR #22618), the membershipCount field for team event groups is intentionally set to 0 in the new createTeamEventGroup function, as confirmed by sean-brydon (PR author). This preserves the same behavior as the old implementation that was being refactored, maintaining backward compatibility. While other parts of the codebase may use actual member counts, this specific implementation maintains the previous behavior.
📚 Learning: 2025-08-05T07:42:06.335Z
Learnt from: sean-brydon
PR: calcom/cal.com#22618
File: packages/trpc/server/routers/viewer/eventTypes/utils/transformUtils.ts:113-113
Timestamp: 2025-08-05T07:42:06.335Z
Learning: In Cal.com's getUserEventGroups handler refactor (PR #22618), the membershipCount field for team event groups is intentionally set to 0 in the new createTeamEventGroup function, as confirmed by sean-brydon (PR author). This preserves the same behavior as the old implementation that was being refactored, maintaining backward compatibility. While other parts of the codebase may use actual member counts, this specific implementation maintains the previous behavior.

Applied to files:

  • packages/trpc/server/routers/viewer/eventTypes/utils/EventTypeGroupFilter.test.ts
📚 Learning: 2025-08-05T07:42:06.335Z
Learnt from: sean-brydon
PR: calcom/cal.com#22618
File: packages/trpc/server/routers/viewer/eventTypes/utils/transformUtils.ts:113-113
Timestamp: 2025-08-05T07:42:06.335Z
Learning: In Cal.com's event type system, the membershipCount field for team event groups is intentionally set to 0, as confirmed by sean-brydon (PR author). This behavior was preserved during the refactor from the old getUserEventGroups.handler.ts implementation to the new createTeamEventGroup function in transformUtils.ts. This is not a bug but the intended behavior that maintains consistency with the previous implementation.

Applied to files:

  • packages/trpc/server/routers/viewer/eventTypes/utils/EventTypeGroupFilter.test.ts
📚 Learning: 2025-08-05T12:04:29.037Z
Learnt from: din-prajapati
PR: calcom/cal.com#21854
File: packages/app-store/office365calendar/__tests__/unit_tests/SubscriptionManager.test.ts:0-0
Timestamp: 2025-08-05T12:04:29.037Z
Learning: In packages/app-store/office365calendar/lib/CalendarService.ts, the fetcher method in Office365CalendarService class is public, not private. It was specifically changed from private to public in this PR to support proper testing and external access patterns.

Applied to files:

  • packages/trpc/server/routers/viewer/eventTypes/utils/EventTypeGroupFilter.test.ts
📚 Learning: 2025-07-21T21:33:23.371Z
Learnt from: Anshumancanrock
PR: calcom/cal.com#22570
File: apps/web/modules/signup-view.tsx:253-253
Timestamp: 2025-07-21T21:33:23.371Z
Learning: In signup-view.tsx, when checking if redirectUrl contains certain strings, using explicit && checks (redirectUrl && redirectUrl.includes()) is preferred over optional chaining (redirectUrl?.includes()) to ensure the result is always a boolean rather than potentially undefined. This approach provides cleaner boolean contracts for downstream conditional logic.

Applied to files:

  • packages/trpc/server/routers/viewer/eventTypes/utils/EventTypeGroupFilter.test.ts
📚 Learning: 2025-08-05T07:42:06.335Z
Learnt from: sean-brydon
PR: calcom/cal.com#22618
File: packages/trpc/server/routers/viewer/eventTypes/utils/transformUtils.ts:113-113
Timestamp: 2025-08-05T07:42:06.335Z
Learning: In Cal.com's event type system, the membershipCount field for team event groups is intentionally hard-coded to 0, as confirmed by sean-brydon. This behavior was preserved during the refactor from the old getUserEventGroups.handler.ts implementation to the new createTeamEventGroup function in transformUtils.ts. This is not a bug but the intended behavior.

Applied to files:

  • packages/trpc/server/routers/viewer/eventTypes/utils/EventTypeGroupFilter.test.ts
🧬 Code Graph Analysis (1)
packages/trpc/server/routers/viewer/eventTypes/utils/EventTypeGroupFilter.test.ts (3)
packages/trpc/server/routers/viewer/eventTypes/utils/transformUtils.ts (1)
  • EventTypeGroup (10-25)
packages/platform/libraries/index.ts (1)
  • MembershipRole (98-98)
packages/trpc/server/routers/viewer/eventTypes/utils/EventTypeGroupFilter.ts (3)
  • filterEvents (119-124)
  • count (100-102)
  • exists (108-110)
🪛 Biome (2.1.2)
packages/trpc/server/routers/viewer/eventTypes/utils/EventTypeGroupFilter.test.ts

[error] 164-164: Expected a semicolon or an implicit semicolon after a statement, but found none

An explicit or implicit semicolon is expected here...

...Which is required to end this statement

(parse)


[error] 165-165: Expected a semicolon or an implicit semicolon after a statement, but found none

An explicit or implicit semicolon is expected here...

...Which is required to end this statement

(parse)


[error] 165-165: Shouldn't redeclare 'result'. Consider to delete it or rename it.

'result' is defined here:

(lint/suspicious/noRedeclare)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Install dependencies / Yarn install & cache

Copy link
Contributor

github-actions bot commented Aug 12, 2025

E2E results are ready!

@CarinaWolli
Copy link
Member

We should probably remove manage already in this PR. It's still showing at the moment:
Screenshot 2025-08-12 at 12 30 46 PM

Copy link
Member

@CarinaWolli CarinaWolli left a comment

Choose a reason for hiding this comment

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

Do we want to include the Workflows and Webhooks tab in this PR as well? That's currently not yet working as it should

Also, another question not related to this PR: Do we plan to also add permissions for Webhooks? I think it would be useful

@sean-brydon
Copy link
Member Author

sean-brydon commented Aug 12, 2025

Do we want to include the Workflows

Yes Ill do that

Also, another question not related to this PR: Do we plan to also add permissions for Webhooks? I think it would be useful

I hadnt thought about it but yeah we for sure can

@sean-brydon
Copy link
Member Author

Edit i have added a stacked PR to handle workflows #23038 inside of event-types

* add permission checks for workflows

* wip getall active workflows

* check eventType.update permission on activiating workflow on event type

* pass permissions to context and use context in client
@keithwillcode keithwillcode requested review from a team as code owners August 26, 2025 11:34
Copy link
Contributor

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

Caution

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

⚠️ Outside diff range comments (2)
packages/trpc/server/routers/viewer/workflows/list.handler.ts (1)

80-101: Prisma: replace include with select and only return needed fields

Per the repository guideline: “For Prisma queries, only select data you need; never use include, always use select.” These blocks use include and also pull potentially heavy relations (e.g., team.members). This increases payload size and coupling, and risks accidental leakage if models gain new fields.

Action:

  • Convert the includes to selects.
  • Ensure you explicitly select the minimal scalar fields required by:
    • the UI list (WorkflowType),
    • readOnly computation (or drop local readOnly logic—see separate comment),
    • PBAC augmentation (addPermissionsToWorkflows requires at least id, teamId, userId).
  • Avoid fetching full team.members; if you still need it locally, narrow to the current user with where and select of just { userId, role, accepted }.

Because the exact UI field requirements live in WorkflowType, prefer centralizing a shared select shape (e.g., workflowListSelect) and reuse it across all four queries.

Example pattern (illustrative; adjust fields to match WorkflowType and what UI renders):

// Define once near the top or in a shared repo file
const workflowListSelect = {
  id: true,
  userId: true,
  teamId: true,
  name: true,          // or title, adjust to actual model
  isActiveOnAll: true, // if used by UI
  // Only what the UI needs:
  team: {
    select: {
      id: true,
      slug: true,
      name: true,
      // If you still compute readOnly here (not recommended), narrow members:
      // members: { where: { userId: ctx.user.id }, select: { userId: true, role: true, accepted: true } }
    },
  },
  activeOn: {
    select: {
      eventType: {
        select: {
          id: true,
          title: true,
          parentId: true,
          _count: { select: { children: true } },
        },
      },
    },
  },
  activeOnTeams: {
    select: {
      team: { select: { id: true, name: true } },
    },
  },
  // If steps are displayed, select only fields needed for the list
  steps: {
    select: {
      id: true,
      // add any minimal fields required by the list (avoid secrets or credentials)
    },
  },
} satisfies Prisma.WorkflowSelect;

// Usage:
const teamWorkflows = await prisma.workflow.findMany({
  where: { /* ... */ },
  select: workflowListSelect,
});

Note:

  • Keep credential fields (like credential.key) out of all selects as per security guideline.
  • addPermissionsToWorkflows must receive id, teamId, userId; ensure they are present in your select.

I can help extract a safe workflowListSelect by diffing the WorkflowType and the list page usage.

Also applies to: 122-148, 176-202, 235-261

packages/features/eventtypes/components/tabs/workflows/EventWorkfowsTab.tsx (1)

135-143: Don’t bypass legacy readOnly locks; combine PBAC and managed-lock checks for edit/toggle.

Right now the Edit button and Switch enablement rely solely on permissions?.canUpdate. If readOnly is true for a children-managed event type, the UI still renders Edit/toggle interactive (just showing a lock icon). That’s inconsistent with the lock semantics.

Apply this patch to derive a single canUpdate flag that respects both PBAC and the managed lock:

@@
   const utils = trpc.useUtils();
+  // Respect both managed lock and PBAC
+  const isLockedByAdmin = workflow.readOnly && props.isChildrenManagedEventType;
+  const canUpdate = (workflow.permissions?.canUpdate ?? true) && !isLockedByAdmin;
@@
-        {workflow.permissions?.canUpdate && (
+        {canUpdate && (
           <div className="flex-none">
@@
-            {workflow.readOnly && props.isChildrenManagedEventType && (
+            {isLockedByAdmin && (
               <Icon name="lock" className="text-subtle h-4 w-4 ltr:mr-2 rtl:ml-2" />
             )}
             <Switch
               checked={isActive}
-              disabled={!workflow.permissions?.canUpdate}
+              disabled={!canUpdate}
               onCheckedChange={() => {
                 activateEventTypeMutation.mutate({ workflowId: workflow.id, eventTypeId: eventType.id });
               }}
             />

Also applies to: 159-164

🧹 Nitpick comments (23)
packages/trpc/server/routers/viewer/workflows/activateEventType.handler.ts (1)

98-112: Reuse PermissionCheckService and refine permission scope & error code

Great job enforcing PBAC for team event types and correctly using the authenticated user’s ID rather than an input parameter. A few optional refinements:

• Module-level service instance
Move new PermissionCheckService() out of the handler and reuse a single instance:

// At the top of activateEventType.handler.ts
const permissionCheckService = new PermissionCheckService();
   if (eventType.teamId) {
-    const permissionCheckService = new PermissionCheckService();
     const hasPermissionToActivate = await permissionCheckService.checkPermission({
       userId: ctx.user.id,
       teamId: eventType.teamId,
       permission: "eventType.update",
       fallbackRoles: [MembershipRole.ADMIN, MembershipRole.OWNER],
     });

• Permission scope (optional least-privilege)
Since we have a workflow.update permission defined for workflows, you could switch to that for toggling workflow activation:

       // permission: "eventType.update",
-      permission: "eventType.update",
+      permission: "workflow.update",

If you’d rather keep "eventType.update" for now, plan a follow-up when a more granular workflow-scoped permission ships.

• Error code consistency (optional)
For authenticated users lacking rights, you might use TRPCError({ code: "FORBIDDEN" }) to distinguish “not allowed” from “not authenticated.” If consistency with existing handlers is more important, retaining "UNAUTHORIZED" is fine.

packages/trpc/server/routers/viewer/workflows/getAllActiveWorkflows.handler.ts (2)

40-40: Consider injecting/reusing PermissionCheckService

Constructing PermissionCheckService per request is fine; however, consider one of:

  • Inject via ctx for easier testing/mocking.
  • Reuse a shared instance if the service is stateless (it appears to be), to avoid repeated instantiation overhead.

Optional, not blocking.


43-51: Do authorization checks before fetching workflows

Currently, workflows are fetched (Lines 20-27) before permission checks, then discarded on UNAUTHORIZED. This adds DB load and increases risk of accidental data leakage via logs/metrics. Recommend moving the workflow fetch until after the authorization block completes. See unified refactor in the next comment’s diff.

Please confirm there’s no intentional side-effect from getEventTypeWorkflows that you rely on before auth.

packages/trpc/server/routers/viewer/workflows/list.handler.ts (4)

6-6: ctx.prisma is typed but never used; adopt it or remove it

We’re importing the PrismaClient type and declaring ctx.prisma but still using the global prisma import. Prefer the request-scoped client from ctx to enable per-request transactions, tracing, and easier testing; otherwise drop the unused ctx.prisma from types.

Option A (recommended): switch to ctx.prisma

-import { prisma } from "@calcom/prisma";
-import type { PrismaClient } from "@calcom/prisma";
+// Use request-scoped Prisma from ctx for better tx scoping and testability
+import type { PrismaClient } from "@calcom/prisma";
@@
 export const listHandler = async ({ ctx, input }: ListOptions) => {
+  const prisma = ctx.prisma;

Option B (minimal): remove ctx.prisma from types if sticking with global prisma

-import type { PrismaClient } from "@calcom/prisma";
@@
 type ListOptions = {
   ctx: {
     user: NonNullable<TrpcSessionUser>;
-    prisma: PrismaClient;
   };
   input: TListInputSchema;
 };

Also applies to: 15-16


103-106: Avoid duplicating readOnly logic; rely on WorkflowPermissionsBuilder

You precompute readOnly for org (force true) and for team/all branches by inspecting team.members. addPermissionsToWorkflows already returns readOnly, so this duplication creates potential drift and extra DB payload (members).

Prefer to drop local readOnly and rely solely on the builder’s value. Keep isOrg tagging if the UI needs it.

@@
-    workflows.push(
-      ...activeOrgWorkflows.map((workflow) => {
-        return { ...workflow, isOrg: true, readOnly: true };
-      })
-    );
+    workflows.push(...activeOrgWorkflows.map((workflow) => ({ ...workflow, isOrg: true })));
@@
-    const workflowsWithReadOnly = teamWorkflows.map((workflow) => {
-      const readOnly = !!workflow.team?.members?.find(
-        (member) => member.userId === ctx.user.id && member.role === MembershipRole.MEMBER
-      );
-      return { ...workflow, readOnly };
-    });
-
-    workflows.push(...workflowsWithReadOnly);
+    workflows.push(...teamWorkflows);
@@
-  const workflowsWithReadOnly: WorkflowType[] = allWorkflows.map((workflow) => {
-    const readOnly = !!workflow.team?.members?.find(
-      (member) => member.userId === ctx.user.id && member.role === MembershipRole.MEMBER
-    );
-
-    return { readOnly, ...workflow };
-  });
-
-  workflows.push(...workflowsWithReadOnly);
+  workflows.push(...allWorkflows);

Benefits:

  • Less data fetched (you can drop team.members from selects).
  • Single source of truth for readOnly.

Also applies to: 153-160, 267-273


162-169: Minor: remove repetition by extracting an augment-and-filter helper

The same addPermissionsToWorkflows + filter chain appears in three places. Extracting a small helper keeps the handler focused and reduces mistakes.

async function augmentAndFilter<T extends { id: number; teamId: number | null; userId: number | null }>(
  items: T[],
  currentUserId: number
) {
  const withPerms = await addPermissionsToWorkflows(items, currentUserId);
  return withPerms.filter((w) => w.permissions.canView);
}

// Usage:
// const filteredWorkflows = await augmentAndFilter(workflows, ctx.user.id);
// return { workflows: filteredWorkflows };

Also applies to: 210-217, 277-284


80-101: If keeping team.members temporarily, at least narrow the selection

Until the full include→select refactor lands, avoid fetching entire member objects. Narrow to the current user and just the fields needed for readOnly.

-        team: {
-          select: {
-            id: true,
-            slug: true,
-            name: true,
-            members: true,
-          },
-        },
+        team: {
+          select: {
+            id: true,
+            slug: true,
+            name: true,
+            members: {
+              where: { userId: ctx.user.id },
+              select: { userId: true, role: true, accepted: true },
+            },
+          },
+        },

Repeat for the similar blocks below.

Also applies to: 122-148, 176-202, 235-261

packages/features/eventtypes/components/tabs/workflows/EventWorkfowsTab.tsx (4)

145-153: Clarify tooltip when action is disabled due to permissions.

Tooltip currently shows “turn on/off” unless readOnly && children-managed. When disabled by PBAC (no canUpdate) but not readOnly, users still see “turn on/off” which is misleading.

-        <Tooltip
-          content={
-            t(
-              workflow.readOnly && props.isChildrenManagedEventType
-                ? "locked_by_team_admin"
-                : isActive
-                ? "turn_off"
-                : "turn_on"
-            ) as string
-          }>
+        <Tooltip
+          content={
+            t(
+              isLockedByAdmin
+                ? "locked_by_team_admin"
+                : canUpdate
+                ? (isActive ? "turn_off" : "turn_on")
+                : "permission_denied" // ensure this i18n key exists
+            ) as string
+          }>

116-121: Localize “Untitled (…)” per project i18n rule for TSX.

Replace the raw “Untitled” with t("untitled") to comply with the localization guideline.

-            {workflow.name
-              ? workflow.name
-              : `Untitled (${`${t(`${workflow.steps[0].action.toLowerCase()}_action`)}`
-                  .charAt(0)
-                  .toUpperCase()}${`${t(`${workflow.steps[0].action.toLowerCase()}_action`)}`.slice(1)})`}
+            {workflow.name
+              ? workflow.name
+              : `${t("untitled")} (${`${t(`${workflow.steps[0].action.toLowerCase()}_action`)}`
+                  .charAt(0)
+                  .toUpperCase()}${`${t(`${workflow.steps[0].action.toLowerCase()}_action`)}`.slice(1)})`}

93-99: Avoid adding empty recipients to the “to:” list.

sendTo.add(step.sendTo || "") can introduce an empty recipient entry. Minor polish to skip falsy values.

-      case WorkflowActions.EMAIL_ADDRESS:
-        sendTo.add(step.sendTo || "");
+      case WorkflowActions.EMAIL_ADDRESS:
+        if (step.sendTo) sendTo.add(step.sendTo);
         break;

1-1: File name typo (“EventWorkfowsTab”).

Consider renaming the file to EventWorkflowsTab.tsx for consistency with the component name and discoverability.

packages/platform/atoms/event-types/hooks/useTabsNavigations.tsx (3)

225-231: Localize “apps” label inside the info string.

The literal “apps” is embedded in a user-facing string in TSX. Prefer a single translatable string with count placeholders.

-      info: `${installedAppsNumber} apps, ${enabledAppsNumber} ${t("active")}`,
+      info: t("apps_summary", { count: installedAppsNumber, active: enabledAppsNumber }),

Follow-up: add apps_summary to locale files with pluralization.


146-154: Localize “Cal.ai” tab title.

Raw string should use t() in TSX to satisfy localization checks.

-        name: "Cal.ai",
+        name: t("cal_ai"),

74-86: Stabilize the useMemo dependency on id.

Calling formMethods.getValues("id") inside the dependency array re-evaluates each render. Capture it into a const and reference the const in both the call and dependencies.

-  const EventTypeTabs = useMemo(() => {
+  const eventTypeId = formMethods.getValues("id");
+  const EventTypeTabs = useMemo(() => {
     const navigation: VerticalTabItemProps[] = getNavigation({
       t,
       length,
       multipleDuration,
-      id: formMethods.getValues("id"),
+      id: eventTypeId,
@@
-  }, [
+  }, [
     t,
@@
-    formMethods.getValues("id"),
+    eventTypeId,
     watchSchedulingType,
@@
     canReadWorkflows,
   ]);

Also applies to: 172-173

packages/features/pbac/client/context/EventPermissionContext.tsx (1)

61-69: Helpful error message, consider adding a dev-friendly hint.

The thrown error is fine. Optionally append which component attempted to read the store (selector.toString()) for faster diagnosis during integration.

apps/web/app/(use-page-wrapper)/event-types/[type]/page.tsx (1)

76-79: Localize user-facing error messages.

Strings “Invalid Event Type id” and “This event type does not exist” are user-facing. Wrap via t() or throw a typed HttpError that maps to localized UI.

Also applies to: 85-86

packages/features/pbac/client/components/WorkflowTabPermissionGuard.tsx (1)

1-24: Simple and effective guard; make permission configurable for reuse.

Hard-coding "workflow.read" limits reuse. Consider accepting a permission prop defaulting to "workflow.read".

+import type { PermissionString } from "../../domain/types/permission-registry";
@@
-interface WorkflowTabPermissionGuardProps {
+interface WorkflowTabPermissionGuardProps {
   children: ReactNode;
   fallback?: ReactNode;
+  permission?: PermissionString;
 }
@@
-export const WorkflowTabPermissionGuard = ({
-  children,
-  fallback = null,
-}: WorkflowTabPermissionGuardProps) => {
-  const { hasPermission } = useWorkflowPermission("workflow.read");
+export const WorkflowTabPermissionGuard = ({
+  children,
+  fallback = null,
+  permission = "workflow.read",
+}: WorkflowTabPermissionGuardProps) => {
+  const { hasPermission } = useWorkflowPermission(permission);
packages/platform/atoms/event-types/wrappers/EventTypeWebWrapper.tsx (3)

111-116: Avoid recreating the permission store on every render

Because EventPermissionProvider internally creates a new store on each render, wrapping here means the store (and subscribers) will be recreated on any parent re-render. That can cause unnecessary re-renders and potential state loss if the store ever becomes stateful.

Refactor the provider to memoize the store once per mount.

Apply in packages/features/pbac/client/context/EventPermissionContext.tsx:

 export const EventPermissionProvider = ({ children, initialPermissions }: EventPermissionProviderProps) => {
-  const store = createEventPermissionStore(initialPermissions);
-
-  return <EventPermissionContext.Provider value={store}>{children}</EventPermissionContext.Provider>;
+  const storeRef = useRef<EventPermissionStore>();
+  if (!storeRef.current) {
+    storeRef.current = createEventPermissionStore(initialPermissions);
+  }
+  return <EventPermissionContext.Provider value={storeRef.current}>{children}</EventPermissionContext.Provider>;
 };

Also applies to: 120-124


151-153: Selector returns an object; prefer primitive to minimize re-renders

useWorkflowPermission currently returns an object { hasPermission, permissions }. Selecting an object in Zustand without an equality function re-renders on every store update. Select the boolean directly.

Change usage here:

-  const { hasPermission: canReadWorkflows } = useWorkflowPermission("workflow.read");
+  const canReadWorkflows = useWorkflowPermission("workflow.read");

And adjust the hook implementation accordingly (see comments in useEventPermission.ts below).


263-268: Refactor Workflows Tab gating for clearer UX and resilience

The Workflows tab is currently only rendered when both allActiveWorkflows (truthy) and canReadWorkflows are true, which can lead to:

  • A flash of missing tab during data loading (when allActiveWorkflows is undefined).
  • Navigation mismatches if the surrounding nav logic only checks permissions.
  • Fragile handling when the workflows list is empty (an empty array is truthy, but gating on presence can still mask the tab during transient states).

Recommend instead gating solely on canReadWorkflows and passing a default empty array into the tab. The tab component itself should handle loading and “no workflows” states internally:

-    workflows:
-      allActiveWorkflows && canReadWorkflows ? (
-        <EventWorkflowsTab eventType={eventType} workflows={allActiveWorkflows} />
-      ) : (
-        <></>
-      ),
+    workflows: canReadWorkflows ? (
+      <EventWorkflowsTab
+        eventType={eventType}
+        workflows={allActiveWorkflows ?? []}
+      />
+    ) : (
+      <></>
+    ),

• Verified that the dynamic import path matches the actual filename (EventWorkfowsTab.tsx) and its default export is correctly named EventWorkflowsTab, so no import/runtime errors will occur.
• This change leaves permission checks in place while ensuring the tab remains present during loading or when no workflows exist.

packages/features/pbac/client/hooks/useEventPermission.ts (3)

6-11: Return a primitive from the selector to prevent needless re-renders

Selecting an object from Zustand without an equality comparator recreates a new object on any store change, causing component re-renders even when the boolean doesn’t change. Return the boolean directly.

-export const useEventTypePermission = (permission: PermissionString) => {
-  return useEventPermissionStore((state) => ({
-    hasPermission: state.hasEventTypePermission(permission),
-    permissions: state.permissions.eventTypes,
-  }));
-};
+export const useEventTypePermission = (permission: PermissionString) => {
+  return useEventPermissionStore((state) => state.hasEventTypePermission(permission));
+};

If consumers need the list of permissions as well, expose a separate hook (e.g., useEventTypePermissionList) to avoid coupling unrelated concerns.


20-25: Same here: return boolean for workflow permissions

Mirror the change for workflows to keep the API consistent and efficient.

-export const useWorkflowPermission = (permission: PermissionString) => {
-  return useEventPermissionStore((state) => ({
-    hasPermission: state.hasWorkflowPermission(permission),
-    permissions: state.permissions.workflows,
-  }));
-};
+export const useWorkflowPermission = (permission: PermissionString) => {
+  return useEventPermissionStore((state) => state.hasWorkflowPermission(permission));
+};

27-32: Optional: split multi-check from list exposure

useWorkflowPermissions returns both hasPermissions and the underlying list. If you want to minimize re-renders, consider returning only the boolean here, and add a dedicated accessor hook for the workflows list (e.g., useWorkflowPermissionList). Not blocking.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 5154e62 and 8f4f177.

📒 Files selected for processing (10)
  • apps/web/app/(use-page-wrapper)/event-types/[type]/page.tsx (3 hunks)
  • packages/features/eventtypes/components/tabs/workflows/EventWorkfowsTab.tsx (4 hunks)
  • packages/features/pbac/client/components/WorkflowTabPermissionGuard.tsx (1 hunks)
  • packages/features/pbac/client/context/EventPermissionContext.tsx (1 hunks)
  • packages/features/pbac/client/hooks/useEventPermission.ts (1 hunks)
  • packages/platform/atoms/event-types/hooks/useTabsNavigations.tsx (6 hunks)
  • packages/platform/atoms/event-types/wrappers/EventTypeWebWrapper.tsx (5 hunks)
  • packages/trpc/server/routers/viewer/workflows/activateEventType.handler.ts (2 hunks)
  • packages/trpc/server/routers/viewer/workflows/getAllActiveWorkflows.handler.ts (2 hunks)
  • packages/trpc/server/routers/viewer/workflows/list.handler.ts (5 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.tsx

📄 CodeRabbit inference engine (.cursor/rules/review.mdc)

Always use t() for text localization in frontend code; direct text embedding should trigger a warning

Files:

  • apps/web/app/(use-page-wrapper)/event-types/[type]/page.tsx
  • packages/features/pbac/client/components/WorkflowTabPermissionGuard.tsx
  • packages/platform/atoms/event-types/hooks/useTabsNavigations.tsx
  • packages/features/pbac/client/context/EventPermissionContext.tsx
  • packages/features/eventtypes/components/tabs/workflows/EventWorkfowsTab.tsx
  • packages/platform/atoms/event-types/wrappers/EventTypeWebWrapper.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/review.mdc)

Flag excessive Day.js use in performance-critical code; prefer native Date or Day.js .utc() in hot paths like loops

Files:

  • apps/web/app/(use-page-wrapper)/event-types/[type]/page.tsx
  • packages/trpc/server/routers/viewer/workflows/getAllActiveWorkflows.handler.ts
  • packages/features/pbac/client/components/WorkflowTabPermissionGuard.tsx
  • packages/trpc/server/routers/viewer/workflows/activateEventType.handler.ts
  • packages/features/pbac/client/hooks/useEventPermission.ts
  • packages/platform/atoms/event-types/hooks/useTabsNavigations.tsx
  • packages/features/pbac/client/context/EventPermissionContext.tsx
  • packages/features/eventtypes/components/tabs/workflows/EventWorkfowsTab.tsx
  • packages/trpc/server/routers/viewer/workflows/list.handler.ts
  • packages/platform/atoms/event-types/wrappers/EventTypeWebWrapper.tsx
**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/review.mdc)

**/*.ts: For Prisma queries, only select data you need; never use include, always use select
Ensure the credential.key field is never returned from tRPC endpoints or APIs

Files:

  • packages/trpc/server/routers/viewer/workflows/getAllActiveWorkflows.handler.ts
  • packages/trpc/server/routers/viewer/workflows/activateEventType.handler.ts
  • packages/features/pbac/client/hooks/useEventPermission.ts
  • packages/trpc/server/routers/viewer/workflows/list.handler.ts
🧠 Learnings (2)
📚 Learning: 2025-08-21T13:44:06.784Z
Learnt from: supalarry
PR: calcom/cal.com#23217
File: apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/output-event-types.service.ts:93-94
Timestamp: 2025-08-21T13:44:06.784Z
Learning: In apps/api/v2/src/ee/event-types/event-types_2024_06_14/event-types.repository.ts, repository functions that use explicit Prisma select clauses (like getEventTypeWithSeats) are used for specific purposes and don't need to include all EventType fields like bookingRequiresAuthentication. These methods don't feed into the general OutputEventTypesService_2024_06_14 flow.

Applied to files:

  • apps/web/app/(use-page-wrapper)/event-types/[type]/page.tsx
📚 Learning: 2025-08-26T08:08:23.383Z
Learnt from: SinghaAnirban005
PR: calcom/cal.com#23343
File: packages/features/insights/server/trpc-router.ts:1080-1101
Timestamp: 2025-08-26T08:08:23.383Z
Learning: In packages/features/insights/server/trpc-router.ts, when filtering personal event types (userId provided, no teamId, not isAll), the query correctly uses user.id (authenticated user) instead of the input userId parameter for security reasons. This prevents users from accessing other users' personal event types by passing arbitrary user IDs.

Applied to files:

  • apps/web/app/(use-page-wrapper)/event-types/[type]/page.tsx
  • packages/trpc/server/routers/viewer/workflows/getAllActiveWorkflows.handler.ts
  • packages/trpc/server/routers/viewer/workflows/activateEventType.handler.ts
🧬 Code graph analysis (7)
apps/web/app/(use-page-wrapper)/event-types/[type]/page.tsx (2)
packages/features/pbac/services/permission-check.service.ts (1)
  • PermissionCheckService (20-370)
packages/platform/atoms/event-types/wrappers/EventTypeWebWrapper.tsx (1)
  • EventTypeWebWrapper (100-125)
packages/trpc/server/routers/viewer/workflows/getAllActiveWorkflows.handler.ts (1)
packages/features/pbac/services/permission-check.service.ts (1)
  • PermissionCheckService (20-370)
packages/trpc/server/routers/viewer/workflows/activateEventType.handler.ts (1)
packages/features/pbac/services/permission-check.service.ts (1)
  • PermissionCheckService (20-370)
packages/features/pbac/client/hooks/useEventPermission.ts (1)
packages/features/pbac/client/context/EventPermissionContext.tsx (1)
  • useEventPermissionStore (61-69)
packages/platform/atoms/event-types/hooks/useTabsNavigations.tsx (1)
packages/ui/components/navigation/index.ts (1)
  • VerticalTabItemProps (6-6)
packages/trpc/server/routers/viewer/workflows/list.handler.ts (2)
packages/prisma/index.ts (1)
  • PrismaClient (83-83)
packages/lib/server/repository/workflow-permissions.ts (1)
  • addPermissionsToWorkflows (164-169)
packages/platform/atoms/event-types/wrappers/EventTypeWebWrapper.tsx (2)
packages/features/pbac/client/context/EventPermissionContext.tsx (2)
  • EventPermissions (8-11)
  • EventPermissionProvider (55-59)
packages/features/pbac/client/hooks/useEventPermission.ts (1)
  • useWorkflowPermission (20-25)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Install dependencies / Yarn install & cache
🔇 Additional comments (15)
packages/trpc/server/routers/viewer/workflows/activateEventType.handler.ts (1)

4-4: PBAC import looks good

Importing PermissionCheckService here aligns this handler with the new PBAC flow. No issues.

packages/trpc/server/routers/viewer/workflows/getAllActiveWorkflows.handler.ts (3)

1-2: Imports align with PBAC integration

Bringing in PermissionCheckService and MembershipRole here is appropriate for the new PBAC flow.


29-34: Personal event-type guard correctly uses authenticated user

Good: you block access when the event type is personal (no team scope) and not owned by the logged-in user. This mirrors our prior learning to rely on ctx.user.id rather than any input-provided user identifier.


54-62: Refactor permission checks to use OR semantics and dedupe team contexts

The current implementation in packages/trpc/server/routers/viewer/workflows/getAllActiveWorkflows.handler.ts requires users to have workflow.read on both eventType.teamId and eventType.parent?.teamId if both are present, which can incorrectly block users who are permitted in one scope but not the other. Instead, we should:

  • Collect a unique list of all relevant team IDs (eventType.teamId and eventType.parent?.teamId).
  • Apply the personal-event guard (no team context and eventType.userId !== ctx.user.id) first.
  • Authorize by checking workflow.read for each team ID and allowing access if any check passes (OR semantics).
  • Defer fetching workflows until after authorization.

Please update the handler accordingly and add tests covering these scenarios:

• Only teamId present (and permitted/not permitted)
• Only parent.teamId present (and permitted/not permitted)
• Both present where one check passes and the other fails
• Personal event types (no team IDs) for owner vs. non-owner
• Fallback roles (ADMIN, OWNER) behavior

Also, confirm the intended policy: should having permission in either the direct team or the parent team be sufficient? If so, this refactor is needed; otherwise, please clarify why both scopes must be enforced.

packages/trpc/server/routers/viewer/workflows/list.handler.ts (3)

4-4: Good call: centralizes PBAC via addPermissionsToWorkflows

Importing addPermissionsToWorkflows and using it to gate visibility by canView aligns with the PBAC approach rolled out elsewhere in the PR.


162-169: Return shape changed to an object; confirm all callers expect { workflows }

Previously, many handlers returned a bare array. Here we now return { workflows: filteredWorkflows } in all branches. Verify the tRPC procedure output schema and all FE callers (e.g., hooks, list page data loaders) to prevent subtle runtime errors.

Suggested checks:

  • Ensure list.procedure’s output schema matches { workflows: ... }.
  • Update any FE usage like const workflows = ... to const { workflows } = ....

Also applies to: 210-217, 277-284


171-176: Check intent: input.userId branch queries by ctx.user.id

In the user branch, we enter when input.userId is provided, but the query uses where: { userId: ctx.user.id }. If the intent is “show my workflows” only, remove the input.userId condition. If admins are allowed to view others’ workflows, consider using input.userId, and rely on PBAC gating to filter what the current user can actually view.

Concrete options:

  • Simplify condition to if (input?.userId ?? ctx.user.id) and query with that ID.
  • Or delete the branch and treat it via the final “allWorkflows” path.

Also applies to: 210-217

packages/features/eventtypes/components/tabs/workflows/EventWorkfowsTab.tsx (1)

228-229: router.replace without await is fine with next/navigation.

router.replace in next/navigation returns void; removing await avoids misleading async flow while preserving navigation.

packages/platform/atoms/event-types/hooks/useTabsNavigations.tsx (2)

234-245: Conditionally adding the Workflows tab looks good.

This keeps the tab out of the nav entirely without permission. Ensure ordering is still as intended relative to later splice/push operations (availability/assignment/recurring/webhooks/instant).

Please validate the final tab order for:

  • personal event types (no team),
  • team event types with and without PBAC.

27-36: canReadWorkflows prop correctly wired from PBAC

I’ve verified that the only call site for useTabsNavigations (in packages/platform/atoms/event-types/wrappers/EventTypeWebWrapper.tsx) retrieves canReadWorkflows via

const { hasPermission: canReadWorkflows } = useWorkflowPermission("workflow.read");

(line 152) and passes it into useTabsNavigations (line 381). No other usages of useTabsNavigations were found, so the new prop is consistently wired from the permission hook. Defaulting to false elsewhere safely hides the workflows tab when not explicitly provided.

apps/web/app/(use-page-wrapper)/event-types/[type]/page.tsx (2)

88-92: Server passes permissions into wrapper: LGTM.

This wires PBAC nicely into the client via EventPermissionProvider.


45-67: Ensure the Workflows tab appears for personal event types

I verified that on the server, getEventPermissions(userId, null) returns an empty workflows array, and on the client the wrapper uses

const { hasPermission: canReadWorkflows } = useWorkflowPermission("workflow.read");
  
useTabsNavigations({, allActiveWorkflows, canReadWorkflows });

Since an empty array ([]) is truthy in JavaScript, the Workflows tab will render whenever canReadWorkflows is true, regardless of teamId. Please manually confirm that this matches the intended behavior for personal (no‐team) event types:

  • Should users with global workflow.read permission see workflows on personal event types?
  • Is there any additional downstream guard or UI logic that needs to derive canReadWorkflows differently when teamId is null?
packages/platform/atoms/event-types/wrappers/EventTypeWebWrapper.tsx (2)

97-104: Safe default to least privilege for permissions prop

Defaulting to empty permission sets is a good defense-in-depth choice that avoids accidental exposure when the server forgets to pass permissions.


381-382: Navigation awareness wired correctly

Passing canReadWorkflows into useTabsNavigations keeps the tabs nav consistent with permission gating.

packages/features/pbac/client/hooks/useEventPermission.ts (1)

34-36: LGTM: full permissions accessor

Returning the entire permissions object is reasonable for top-level consumers that need cross-category visibility.

Comment on lines +28 to +31
type PartialWorkflowType = Pick<
WorkflowType,
"name" | "activeOn" | "isOrg" | "steps" | "id" | "readOnly" | "permissions"
>;
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Adding permissions to PartialWorkflowType is correct; verify typing and payloads match across boundaries.

This makes UI gating straightforward. Please verify:

  • WorkflowType actually contains a permissions field with at least canUpdate.
  • TRPC outputs (viewer.workflows.list/getAllActiveWorkflows) include permissions for both team and personal workflows so we don’t regress when PBAC is disabled.

🏁 Script executed:

#!/usr/bin/env bash
# Verify WorkflowType shape and API payloads include `permissions`
set -euo pipefail

# 1) Find WorkflowType definition(s)
fd -a --hidden --exclude node_modules 'WorkflowListPage' | xargs -I{} rg -n --no-heading 'type\s+WorkflowType|interface\s+WorkflowType|export\s+type\s+WorkflowType' {}

# 2) Ensure `permissions` exists on list/get payloads
rg -n -C2 --type=ts 'workflows\.(list|getAllActiveWorkflows)\.use(Query|Mutation)' packages | cat
rg -n -C2 --type=ts 'permissions.*canUpdate' packages | cat

# 3) Check server router/handlers add `permissions`
fd -a --hidden --exclude node_modules 'workflows' packages/trpc/server/routers | xargs rg -n 'permissions|canUpdate|getResourcePermissions'

Length of output: 7429


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "=== EventWorkfowsTab.tsx Imports and Local Types ==="
head -n 50 packages/features/eventtypes/components/tabs/workflows/EventWorkfowsTab.tsx

echo
echo "=== All ‘export type WorkflowType’ Definitions ==="
rg -n --heading 'export type WorkflowType' -C5 .

echo
echo "=== getAllActiveWorkflows Handler Context ==="
rg -n -C5 'getAllActiveWorkflows' packages/trpc/server/routers

Length of output: 12389


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

# 1. Inspect WorkflowType in WorkflowListPage.tsx for permissions field
rg -n --heading 'export type WorkflowType' -C20 packages/features/ee/workflows/components/WorkflowListPage.tsx

# 2. Inspect util.ts to see if permissions are added in getEventTypeWorkflows
sed -n '1,200p' packages/trpc/server/routers/viewer/workflows/util.ts

Length of output: 7143


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "=== WorkflowType alias in WorkflowListPage.tsx (lines 30-80) ==="
sed -n '30,80p' packages/features/ee/workflows/components/WorkflowListPage.tsx

Length of output: 1573


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "=== list.handler.ts (permission injection) ==="
sed -n '140,260p' packages/trpc/server/routers/viewer/workflows/list.handler.ts

echo
echo "=== getAllActiveWorkflows.handler.ts (permission injection) ==="
sed -n '1,200p' packages/trpc/server/routers/viewer/workflows/getAllActiveWorkflows.handler.ts

echo
echo "=== getEventTypeWorkflows in util.ts ==="
rg -n -C5 'function getEventTypeWorkflows' packages/trpc/server/routers/viewer/workflows/util.ts || echo "getEventTypeWorkflows not found"

echo
echo "=== getAllWorkflowsFromEventType in util.ts ==="
rg -n -C5 'function getAllWorkflowsFromEventType' packages/trpc/server/routers/viewer/workflows/util.ts || echo "getAllWorkflowsFromEventType not found"

Length of output: 6331


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "=== Locate getEventTypeWorkflows Definition ==="
rg -nP -C5 'getEventTypeWorkflows' packages/trpc/server/routers/viewer/workflows/util.ts || echo 'No match'

# Print around the signature if function or const
LINE=$(rg -nP -m1 'export (async )?function getEventTypeWorkflows|export const getEventTypeWorkflows' packages/trpc/server/routers/viewer/workflows/util.ts | cut -d: -f1)
if [ -n "$LINE" ]; then
  START=$((LINE-5))
  END=$((LINE+50))
  echo "=== getEventTypeWorkflows Body ==="
  sed -n "${START},${END}p" packages/trpc/server/routers/viewer/workflows/util.ts
fi

echo
echo "=== getAllWorkflowsFromEventType Body ==="
LINE2=$(rg -nP -m1 'export async function getAllWorkflowsFromEventType' packages/trpc/server/routers/viewer/workflows/util.ts | cut -d: -f1)
if [ -n "$LINE2" ]; then
  START2=$((LINE2-10))
  END2=$((LINE2+200))
  sed -n "${START2},${END2}p" packages/trpc/server/routers/viewer/workflows/util.ts
fi

Length of output: 5697


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

# 1. Locate and inspect the WorkflowPermissions type in the shared types file
echo "=== Locate types.ts for WorkflowPermissions ==="
TYPES_FILE=$(fd -I -t f types.ts packages/features/ee/workflows/lib)
echo "Found: $TYPES_FILE"
sed -n '1,200p' "$TYPES_FILE"

echo
# 2. Locate and inspect getAllWorkflows implementation
echo "=== Locate getAllWorkflows.ts ==="
GETALL_FILE=$(fd -I -t f getAllWorkflows.ts packages/features/ee/workflows/lib)
echo "Found: $GETALL_FILE"
sed -n '1,200p' "$GETALL_FILE"

Length of output: 4410


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "=== Search for WorkflowPermissions definition ==="
rg -n --type=ts "WorkflowPermissions" -C3 packages/features/ee/workflows/lib

Length of output: 205


Ensure getAllActiveWorkflows also injects permissions like list does

While PartialWorkflowType correctly picks up permissions, the TRPC handler for getAllActiveWorkflows currently returns plain workflows without attaching permission metadata. As a result, UI gating (permissions?.canUpdate) will always be undefined (falsy) for “all active” workflows.

Please address the following:

• In packages/trpc/server/routers/viewer/workflows/getAllActiveWorkflows.handler.ts
– After retrieving workflows via getEventTypeWorkflows, call addPermissionsToWorkflows(workflows, ctx.user.id) (as in list.handler.ts) to produce workflowsWithPermissions.
– Filter or return the enriched array instead of the raw objects.
– Update the Zod output schema (getAllActiveWorkflows.schema.ts) to include permissions.

• In packages/trpc/server/routers/viewer/workflows/util.ts
– Ensure that getEventTypeWorkflows and/or getAllWorkflowsFromEventType propagate the newly added permissions field through their return values.

• Verify that the shared WorkflowPermissions type (used in your client-side WorkflowType) defines at least canUpdate (and any other needed flags). Adjust or import the correct type from your PBAC domain (@calcom/features/pbac or your permissions registry) if it’s missing or incomplete.

Once these changes are in place, PartialWorkflowType will reliably receive permissions.canUpdate for both the “list” and “all active” workflows, restoring consistent UI gating.

🤖 Prompt for AI Agents
In packages/features/eventtypes/components/tabs/workflows/EventWorkfowsTab.tsx
around lines 28-31: the TRPC handler getAllActiveWorkflows must return workflows
with permissions like list does; update
packages/trpc/server/routers/viewer/workflows/getAllActiveWorkflows.handler.ts
to call addPermissionsToWorkflows(workflows, ctx.user.id) after
getEventTypeWorkflows and return the resulting workflowsWithPermissions
(filtering/mapping as needed) instead of the raw workflows, update
packages/trpc/server/routers/viewer/workflows/getAllActiveWorkflows.schema.ts to
include the permissions field in the Zod output schema, ensure
packages/trpc/server/routers/viewer/workflows/util.ts propagates the permissions
field from getEventTypeWorkflows / getAllWorkflowsFromEventType through their
return values, and verify/import the correct WorkflowPermissions type (with at
least canUpdate) from the PBAC domain (@calcom/features/pbac or permissions
registry) so the client-side PartialWorkflowType receives permissions.canUpdate.

Comment on lines +22 to +45
const createEventPermissionStore = (
initialPermissions: EventPermissions = { eventTypes: [], workflows: [] }
) =>
createStore<EventPermissionStore>()((set, get) => ({
permissions: initialPermissions,
setPermissions: (permissions) => set({ permissions }),
hasEventTypePermission: (permission) => {
const { permissions } = get();
return permissions.eventTypes.includes(permission);
},
hasWorkflowPermission: (permission) => {
const { permissions } = get();
return permissions.workflows.includes(permission);
},
hasEventTypePermissions: (requiredPermissions) => {
const { permissions } = get();
return requiredPermissions.every((permission) => permissions.eventTypes.includes(permission));
},
hasWorkflowPermissions: (requiredPermissions) => {
const { permissions } = get();
return requiredPermissions.every((permission) => permissions.workflows.includes(permission));
},
}));

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Align client multi-permission checks with server semantics (empty arrays should be false).

Server PermissionCheckService.checkPermissions returns false for empty arrays to avoid accidental privilege grants. The client currently returns true because Array.every([]) === true.

     hasEventTypePermissions: (requiredPermissions) => {
       const { permissions } = get();
-      return requiredPermissions.every((permission) => permissions.eventTypes.includes(permission));
+      if (requiredPermissions.length === 0) return false;
+      return requiredPermissions.every((permission) => permissions.eventTypes.includes(permission));
     },
     hasWorkflowPermissions: (requiredPermissions) => {
       const { permissions } = get();
-      return requiredPermissions.every((permission) => permissions.workflows.includes(permission));
+      if (requiredPermissions.length === 0) return false;
+      return requiredPermissions.every((permission) => permissions.workflows.includes(permission));
     },
🤖 Prompt for AI Agents
In packages/features/pbac/client/context/EventPermissionContext.tsx around lines
22 to 45, the multi-permission checks use Array.every which returns true for
empty arrays, but server semantics require returning false for empty
requiredPermissions; update hasEventTypePermissions and hasWorkflowPermissions
to first check if requiredPermissions is non-empty (e.g.,
requiredPermissions.length > 0) and return false if empty, otherwise perform the
.every(...) check so client behavior matches the server.

Comment on lines +55 to +59
export const EventPermissionProvider = ({ children, initialPermissions }: EventPermissionProviderProps) => {
const store = createEventPermissionStore(initialPermissions);

return <EventPermissionContext.Provider value={store}>{children}</EventPermissionContext.Provider>;
};
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Avoid recreating the Zustand store on every render.

Provider recreates the store each render, resetting subscribers. Memoize via useRef to keep a stable store instance.

-export const EventPermissionProvider = ({ children, initialPermissions }: EventPermissionProviderProps) => {
-  const store = createEventPermissionStore(initialPermissions);
-
-  return <EventPermissionContext.Provider value={store}>{children}</EventPermissionContext.Provider>;
-};
+export const EventPermissionProvider = ({ children, initialPermissions }: EventPermissionProviderProps) => {
+  const storeRef = (globalThis as any).__eventPermissionStoreRef ??= { current: undefined };
+  if (!storeRef.current) {
+    storeRef.current = createEventPermissionStore(initialPermissions);
+  }
+  return <EventPermissionContext.Provider value={storeRef.current}>{children}</EventPermissionContext.Provider>;
+};

Note: If you prefer React’s primitives only, replace the global ref with useRef; ensure this provider renders once at the app wrapper level to keep a single store.

📝 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 const EventPermissionProvider = ({ children, initialPermissions }: EventPermissionProviderProps) => {
const store = createEventPermissionStore(initialPermissions);
return <EventPermissionContext.Provider value={store}>{children}</EventPermissionContext.Provider>;
};
export const EventPermissionProvider = ({ children, initialPermissions }: EventPermissionProviderProps) => {
const storeRef = (globalThis as any).__eventPermissionStoreRef ??= { current: undefined };
if (!storeRef.current) {
storeRef.current = createEventPermissionStore(initialPermissions);
}
return <EventPermissionContext.Provider value={storeRef.current}>{children}</EventPermissionContext.Provider>;
};
🤖 Prompt for AI Agents
In packages/features/pbac/client/context/EventPermissionContext.tsx around lines
55 to 59, the provider currently calls createEventPermissionStore on every
render which recreates the Zustand store and resets subscribers; change it to
create the store only once (e.g. storeRef =
useRef(createEventPermissionStore(initialPermissions)) and use storeRef.current
as the provider value) so the store instance is stable across renders and
initialPermissions is only applied on first render (or handle updates explicitly
if needed).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
consumer core area: core, team members only event-types area: event types, event-types ✨ feature New feature or request ❗️ migrations contains migration files ready-for-e2e
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants