Skip to content

[WEB-4896]feat: filters to project and workspace members list#7786

Merged
pushya22 merged 6 commits intopreviewfrom
feat-project_member_filters
Sep 24, 2025
Merged

[WEB-4896]feat: filters to project and workspace members list#7786
pushya22 merged 6 commits intopreviewfrom
feat-project_member_filters

Conversation

@vamsikrishnamathala
Copy link
Member

@vamsikrishnamathala vamsikrishnamathala commented Sep 12, 2025

Description

This PR introduces comprehensive filtering and sorting capabilities for member lists in both workspace and project settings. Users can now filter members by role and sort by various properties including name, email, joining date, and role.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • Feature (non-breaking change which adds functionality)
  • Improvement (change that would cause existing functionality to not work as expected)
  • Code refactoring
  • Performance improvements
  • Documentation update

Screenshots and Media (if applicable)

Screen.Recording.2025-09-12.at.5.01.10.PM.mov

Test Scenarios

References

Summary by CodeRabbit

  • New Features

    • Role-based filtering for project and workspace member lists via a Filters dropdown (per-project/workspace).
    • Clickable table headers to sort by Full name, Display name, Email, Joining date, or Role.
    • Added Email column to member tables.
  • Enhancements

    • Redesigned members header: unified row with search, role filters, Add Member (admin-only), and billing actions.
    • Search now composes with applied filters for precise results.
  • Localization

    • Added translations for Full name, Display name, Email, Joining date, and Role.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 12, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds per-project and per-workspace role-based filtering and header-driven sorting for member lists by introducing filter stores, filtering/sorting utilities, UI components (filters dropdown, header column), and store/hook APIs to expose and consume filtered member retrieval.

Changes

Cohort / File(s) Summary
Filtering utilities & constants
apps/web/core/store/member/utils.ts, packages/constants/src/members.ts, packages/constants/src/index.ts, packages/i18n/src/locales/en/translations.json
Add IMemberFilters and unified member filtering/sorting utilities; add member ordering constants (TMemberOrderByOptions, MEMBER_PROPERTY_DETAILS); re-export members constants and add i18n keys for project_members.
Project filters store & base integration
apps/web/core/store/member/project/project-member-filters.store.ts, apps/web/core/store/member/project/base-project-member.store.ts, apps/web/ce/store/member/project-member.store.ts
Add ProjectMemberFiltersStore and IProjectMemberFiltersStore; wire filters into BaseProjectMemberStore (filters property, getFilteredProjectMemberDetails, filter-aware project member id sorting); update import path in CE project store.
Workspace filters store & integration
apps/web/core/store/member/workspace/workspace-member-filters.store.ts, apps/web/core/store/member/workspace/workspace-member.store.ts, apps/web/core/store/member/index.ts, apps/web/core/store/issue/root.store.ts
Add WorkspaceMemberFiltersStore and IWorkspaceMemberFiltersStore; expose workspace.filtersStore and getFilteredWorkspaceMemberIds; update related import paths.
Shared UI components (filters + header)
apps/web/core/components/project/dropdowns/filters/member-list.tsx, apps/web/core/components/project/member-header-column.tsx
Add MemberListFilters, MemberListFiltersDropdown, and MemberHeaderColumn to render role filter options and header sorting controls; components accept appliedFilters/displayFilters and call provided update handlers.
Project members list UI changes
apps/web/core/components/project/member-list.tsx
Replace member accessors with filter-aware ones (getFilteredProjectMemberDetails), wire MemberListFiltersDropdown and project filters API, add handleRoleFilterUpdate to toggle role tokens, consolidate header controls.
Workspace members list & settings UI
apps/web/core/components/workspace/settings/members-list.tsx, apps/web/ce/components/workspace/settings/useMemberColumns.tsx, apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/members/page.tsx
Consume workspace.filtersStore and getFilteredWorkspaceMemberIds, derive searched/filtered member ids using new APIs, add MemberHeaderColumn in headers, and integrate MemberListFiltersDropdown with role-filter update handlers in the settings header.
Project columns & CE hooks
apps/web/ce/components/projects/settings/useProjectColumns.tsx
Render MemberHeaderColumn in thRender for columns, derive displayFilters via useMember, add handleDisplayFilterUpdate, and extend hook return to include displayFilters and handler.
Import path adjustments / minor updates
apps/web/core/components/project/member-list-item.tsx, apps/web/ce/store/member/project-member.store.ts, apps/web/core/store/member/index.ts, apps/web/core/store/issue/root.store.ts
Update import paths for member store modules (project/workspace) and adjust module resolutions; no behavioral changes beyond path adjustments.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant Header as Header (Search / Filters)
  participant Filters as FiltersStore (project/workspace)
  participant Store as MemberStore (project/workspace)
  participant View as Member List View

  User->>Header: toggle role / change sort / search
  Header->>Filters: updateFilters(projectId?, { roles/order_by })
  Filters-->>Store: expose current filters (getFilters / filtersMap)
  Store->>Store: compute filtered & sorted member IDs (sortProject/sortWorkspace)
  Store-->>View: return filtered member IDs / member details
  View-->>User: render updated member list

  rect rgba(233,246,255,0.5)
    note right of Header: MemberListFiltersDropdown & MemberHeaderColumn trigger updates
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

🌐frontend

Suggested reviewers

  • prateekshourya29
  • sriramveeraghanta

Poem

I hop through columns, badges bright,
Toggling roles with pure delight.
Dropdowns open, filters sing,
Members line up — sorted ring.
A carrot nod — the list feels right 🥕

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title Check ✅ Passed The title "[WEB-4896]feat: filters to project and workspace members list" concisely and accurately summarizes the main change—adding filtering functionality to project and workspace member lists—so it is fully related and clear for reviewers; the ticket tag and "feat" prefix are appropriate, though there is a minor spacing/style nit (missing space after the ticket tag).
Description Check ✅ Passed The PR description follows the repository template and provides a clear Description and a correctly marked Type of Change, and it includes a Screenshots link, but the Test Scenarios section is empty and References contains no related issue links, leaving reviewers without information on how the change was tested or linked tasks; overall the description is mostly complete and the intent is clear.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ 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-project_member_filters

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.

@makeplane
Copy link

makeplane bot commented Sep 12, 2025

Linked to Plane Work Item(s)

This comment was auto-generated by Plane

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: 10

Caution

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

⚠️ Outside diff range comments (2)
apps/web/core/store/issue/root.store.ts (1)

229-231: Bug: workSpaceMemberRolesMap is assigned the wrong map — use workspaceMemberMap, not memberMap

Location: apps/web/core/store/issue/root.store.ts (lines 229-231)

Assigning rootStore?.memberRoot?.workspace?.memberMap (IUser map) to workSpaceMemberRolesMap (expects IWorkspaceMembership map) is incorrect.

-      if (!isEmpty(rootStore?.memberRoot?.workspace?.workspaceMemberMap))
-        this.workSpaceMemberRolesMap = rootStore?.memberRoot?.workspace?.memberMap || undefined;
+      if (!isEmpty(rootStore?.memberRoot?.workspace?.workspaceMemberMap))
+        this.workSpaceMemberRolesMap = rootStore?.memberRoot?.workspace?.workspaceMemberMap || undefined;
apps/web/core/store/member/workspace/workspace-member.store.ts (1)

159-173: Search currently ignores filters; search within the filtered set.

Apply search on top of getFilteredWorkspaceMemberIds so results respect role filters.

   getSearchedWorkspaceMemberIds = computedFn((searchQuery: string) => {
     const workspaceSlug = this.routerStore.workspaceSlug;
     if (!workspaceSlug) return null;
-    const workspaceMemberIds = this.workspaceMemberIds;
+    // base list should respect current filters
+    const workspaceMemberIds = this.getFilteredWorkspaceMemberIds(workspaceSlug);
     if (!workspaceMemberIds) return null;
     const searchedWorkspaceMemberIds = workspaceMemberIds?.filter((userId) => {
       const memberDetails = this.getWorkspaceMemberDetails(userId);
       if (!memberDetails) return false;
       const memberSearchQuery = `${memberDetails.member.first_name} ${memberDetails.member.last_name} ${
         memberDetails.member?.display_name
       } ${memberDetails.member.email ?? ""}`;
       return memberSearchQuery.toLowerCase()?.includes(searchQuery.toLowerCase());
     });
     return searchedWorkspaceMemberIds;
   });
🧹 Nitpick comments (33)
apps/web/core/components/project/dropdowns/filters/member-list.tsx (3)

22-33: Localize labels (“Admin/Member/Guest”, “Roles”, “Filters”).

Hardcoded strings bypass i18n. Use translations (e.g., role_details.*.title, common.filters, project_members.roles).

Apply:

@@
-import { Button, CustomMenu } from "@plane/ui";
+import { Button, CustomMenu } from "@plane/ui";
+import { useTranslation } from "@plane/i18n";
@@
-const PROJECT_ROLE_OPTIONS: IRoleOption[] = [
-  { value: String(EUserProjectRoles.ADMIN), label: "Admin" },
-  { value: String(EUserProjectRoles.MEMBER), label: "Member" },
-  { value: String(EUserProjectRoles.GUEST), label: "Guest" },
-];
+const PROJECT_ROLE_OPTIONS: IRoleOption[] = [
+  { value: String(EUserProjectRoles.ADMIN), label: "Admin" },
+  { value: String(EUserProjectRoles.MEMBER), label: "Member" },
+  { value: String(EUserProjectRoles.GUEST), label: "Guest" },
+];
@@
-  const [isExpanded, setIsExpanded] = useState(true);
+  const { t } = useTranslation();
+  const [isExpanded, setIsExpanded] = useState(true);
@@
-      <FilterHeader
-        title={`Roles${appliedFiltersCount > 0 ? ` (${appliedFiltersCount})` : ""}`}
+      <FilterHeader
+        title={`${t("project_members.roles")}${appliedFiltersCount > 0 ? ` (${appliedFiltersCount})` : ""}`}
         isPreviewEnabled={isExpanded}
         handleIsPreviewEnabled={() => setIsExpanded(!isExpanded)}
       />
@@
-                title={role.label}
+                title={t(`role_details.${role.label.toLowerCase()}.title`)}
                 onClick={() => handleUpdate(role.value)}
               />
@@
-export const MemberListFiltersDropdown: React.FC<Props> = observer((props) => {
+export const MemberListFiltersDropdown: React.FC<Props> = observer((props) => {
   const { appliedFilters, handleUpdate, memberType } = props;
+  const { t } = useTranslation();
@@
-          <Button variant="neutral-primary" size="sm" className="flex items-center gap-2">
-            <span>Filters</span>
+          <Button variant="neutral-primary" size="sm" className="flex items-center gap-2">
+            <span>{t("common.filters")}</span>
             <ChevronDown className="h-3 w-3" />
           </Button>

Note: Add "project_members.roles" to translations (see comment in translations.json).

Also applies to: 47-50, 92-95


41-43: Count only role filters applied to this group.

Using total length may miscount if other filters are added later. Intersect with role options.

-  const appliedFiltersCount = appliedFilters?.length ?? 0;
-  const roleOptions = memberType === "project" ? PROJECT_ROLE_OPTIONS : WORKSPACE_ROLE_OPTIONS;
+  const roleOptions = memberType === "project" ? PROJECT_ROLE_OPTIONS : WORKSPACE_ROLE_OPTIONS;
+  const appliedFiltersCount =
+    appliedFilters?.filter((v) => roleOptions.some((o) => o.value === v)).length ?? 0;

96-99: A11y: annotate the badge dot for screen readers.

The status dot has no accessible name.

-          {appliedFiltersCount > 0 && (
-            <div className="absolute -top-1 -right-1 h-2 w-2 rounded-full bg-custom-primary-100" />
-          )}
+          {appliedFiltersCount > 0 && (
+            <>
+              <div className="absolute -top-1 -right-1 h-2 w-2 rounded-full bg-custom-primary-100" />
+              <span className="sr-only">{appliedFiltersCount} filters applied</span>
+            </>
+          )}
packages/i18n/src/locales/en/translations.json (1)

2409-2415: Add plural label for roles to support the filter header.

Current code needs “Roles”; add a key to avoid concatenating/pluralizing manually.

   "project_members": {
+    "roles": "Roles",
     "full_name": "Full name",
     "display_name": "Display name",
     "email": "Email",
     "joining_date": "Joining date",
     "role": "Role"
   }
apps/web/core/components/workspace/settings/members-list.tsx (3)

53-55: Search should respect active filters (compose, don’t override).

Currently, any search ignores filter results. Intersect search with filtered IDs.

-  const filteredMemberIds = workspaceSlug ? getFilteredWorkspaceMemberIds(workspaceSlug.toString()) : [];
-  const searchedMemberIds = searchQuery ? getSearchedWorkspaceMemberIds(searchQuery) : filteredMemberIds;
+  const ws = typeof workspaceSlug === "string" ? workspaceSlug : workspaceSlug?.[0];
+  const filteredMemberIds = ws ? getFilteredWorkspaceMemberIds(ws) : [];
+  const searchedMemberIds = searchQuery
+    ? getSearchedWorkspaceMemberIds(searchQuery).filter((id) => filteredMemberIds.includes(id))
+    : filteredMemberIds;

If preferred, expose a store helper like getSearchedWorkspaceMemberIdsWithin(query, withinIds).


41-46: Avoid toString on possibly-array params.

useParams() can return string | string[]; pick the first segment explicitly.

-    workspaceSlug
-      ? async () => {
-          await fetchWorkspaceMemberInvitations(workspaceSlug.toString());
-          await fetchWorkspaceMembers(workspaceSlug.toString());
+    ws
+      ? async () => {
+          await fetchWorkspaceMemberInvitations(ws);
+          await fetchWorkspaceMembers(ws);
         }
-      : null
+      : null

(Assumes ws as introduced in the previous diff.)


60-60: Trailing tab in className.

Minor formatting nit: remove the stray tab after “overflow-scroll”.

-      <div className="divide-y-[0.5px] divide-custom-border-100 overflow-scroll	">
+      <div className="divide-y-[0.5px] divide-custom-border-100 overflow-scroll">
apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/members/page.tsx (1)

132-143: Add basic a11y and avoid focus stealing on search.

Prevent unexpected autofocus and add an accessible label.

-              <input
+              <input
                 className="w-full max-w-[234px] border-none bg-transparent text-sm outline-none placeholder:text-custom-text-400"
-                placeholder={`${t("search")}...`}
+                placeholder={`${t("search")}...`}
+                aria-label={t("search")}
                 value={searchQuery}
-                autoFocus
                 onChange={(e) => setSearchQuery(e.target.value)}
               />
apps/web/ce/components/projects/settings/useProjectColumns.tsx (4)

38-43: Pass project context to permission check.

To avoid relying on router state, pass explicit scope.

-  const isAdmin = allowPermissions(
-    [EUserPermissions.ADMIN],
-    EUserPermissionsLevel.PROJECT,
-    workspaceSlug.toString(),
-    projectId.toString()
-  );
+  const isAdmin = allowPermissions(
+    [EUserPermissions.ADMIN],
+    EUserPermissionsLevel.PROJECT,
+    workspaceSlug.toString(),
+    projectId.toString()
+  );

47-52: Guard against invalid/empty dates.

new Date("") is invalid. Provide a safe fallback.

-  const getFormattedDate = (dateStr: string) => {
-    const date = new Date(dateStr);
-    const options: Intl.DateTimeFormatOptions = { year: "numeric", month: "long", day: "numeric" };
-    return date.toLocaleDateString("en-US", options);
-  };
+  const getFormattedDate = (dateStr: string) => {
+    if (!dateStr) return "-";
+    const d = new Date(dateStr);
+    if (isNaN(d.getTime())) return "-";
+    return d.toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" });
+  };

95-106: Email cell should handle missing values.

Avoid rendering undefined and add truncation width.

-      tdRender: (rowData: RowData) => <div className="w-48 text-custom-text-200">{rowData.member.email}</div>,
+      tdRender: (rowData: RowData) => (
+        <div className="w-48 truncate text-custom-text-200" title={rowData.member.email || "-"}>
+          {rowData.member.email || "-"}
+        </div>
+      ),

61-138: Memoize columns to reduce re-renders.

These objects are recreated every render; wrap in useMemo keyed by the few deps.

apps/web/core/store/member/workspace/workspace-member.store.ts (1)

342-346: JSDoc param name is wrong.

The docs say memberId but the function takes invitationId.

-   * @param memberId
+   * @param invitationId
apps/web/core/components/project/member-header-column.tsx (2)

65-99: Localize sorting labels.

ascendingOrderTitle/descendingOrderTitle are raw strings (“A/Z”, “Old/New”, “Guest/Admin”). Consider moving them to i18n (or provide i18n keys in constants) for full localization.


39-44: Keyboard accessibility.

customButtonTabIndex={-1} with a div may hinder keyboard users. Consider making the trigger a button with proper focus order and ARIA.

apps/web/core/components/project/member-list.tsx (4)

37-46: Include email in search and guard nullable fields.

Avoid potential toLowerCase() on undefined and align search with workspace list (which includes email).

-  const searchedProjectMembers = (projectMemberIds ?? []).filter((userId) => {
-    const memberDetails = projectId ? getFilteredProjectMemberDetails(userId, projectId.toString()) : null;
-
-    if (!memberDetails?.member || !memberDetails.original_role) return false;
-
-    const fullName = `${memberDetails?.member.first_name} ${memberDetails?.member.last_name}`.toLowerCase();
-    const displayName = memberDetails?.member.display_name.toLowerCase();
-
-    return displayName?.includes(searchQuery.toLowerCase()) || fullName.includes(searchQuery.toLowerCase());
-  });
+  const searchedProjectMembers = (projectMemberIds ?? []).filter((userId) => {
+    const memberDetails = projectId ? getFilteredProjectMemberDetails(userId, projectId.toString()) : null;
+    if (!memberDetails?.member || !memberDetails.original_role) return false;
+    const fullName = `${memberDetails.member.first_name ?? ""} ${memberDetails.member.last_name ?? ""}`.toLowerCase();
+    const displayName = (memberDetails.member.display_name ?? "").toLowerCase();
+    const email = (memberDetails.member.email ?? "").toLowerCase();
+    const q = searchQuery.toLowerCase();
+    return displayName.includes(q) || fullName.includes(q) || email.includes(q);
+  });

83-92: Add a11y label and avoid autofocus on search.

Keep behavior consistent with workspace settings.

-            <input
+            <input
               className="w-full max-w-[234px] border-none bg-transparent text-sm focus:outline-none placeholder:text-custom-text-400"
-              placeholder="Search"
+              placeholder={t("search")}
+              aria-label={t("search")}
               value={searchQuery}
-              autoFocus
               onChange={(e) => setSearchQuery(e.target.value)}
             />

52-52: Permission scope: pass explicit context.

Align with other usages to avoid depending on router state.

-  const isAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT);
+  const isAdmin = allowPermissions(
+    [EUserPermissions.ADMIN],
+    EUserPermissionsLevel.PROJECT,
+    workspaceSlug,
+    projectId
+  );

48-51: Avoid recomputing member details twice.

You call getFilteredProjectMemberDetails in both the filter and the mapping. Cache the details or map once for minor perf gains.

Also applies to: 116-121

apps/web/ce/components/workspace/settings/useMemberColumns.tsx (2)

46-52: Unify header labels to avoid mismatched i18n between content and thRender.

Right now thRender shows project_members.* while content uses workspace_settings.*. Pick one source for the visible header and assistive text to keep SR/exports consistent.

Apply this (example: use thRender everywhere and make content a plain mirror):

-      content: t("workspace_settings.settings.members.details.full_name"),
+      content: t("project_members.full_name"),

Repeat for Display name, Email address, Account type, Joining date.

Also applies to: 67-73, 80-86, 93-99, 114-120, 43-46, 65-66, 78-79, 91-92, 112-113


36-39: Memoize handlers and columns to reduce header re-renders.

MemberHeaderColumn receives a new handler and a new columns array each render. Memoize both.

+import { useCallback, useMemo } from "react";
@@
-  const handleDisplayFilterUpdate = (filterUpdates: Partial<IMemberFilters>) => {
-    updateFilters(filterUpdates);
-  };
+  const handleDisplayFilterUpdate = useCallback(
+    (filterUpdates: Partial<IMemberFilters>) => updateFilters(filterUpdates),
+    [updateFilters]
+  );
@@
-  const columns = [
+  const columns = useMemo(
+    () => [
       { /* ...unchanged... */ },
       { /* ...unchanged... */ },
       { /* ...unchanged... */ },
       { /* ...unchanged... */ },
       { /* ...unchanged... */ },
-  ];
+    ],
+    [t, filters, handleDisplayFilterUpdate, workspaceSlug, isAdmin, currentUser]
+  );

Also applies to: 41-63, 64-76, 77-89, 90-101, 111-123

apps/web/core/store/member/project/project-member-filters.store.ts (2)

25-33: Prefer observable maps for dynamic per-project keys.

MobX observable Map better models dynamic IDs and avoids edge cases with late-added object keys.

-import { action, makeObservable, observable } from "mobx";
+import { action, makeObservable, observable } from "mobx";
@@
-  filtersMap: Record<string, IMemberFilters> = {};
+  filtersMap: Map<string, IMemberFilters> = observable.map<string, IMemberFilters>();
@@
-      filtersMap: observable,
+      filtersMap: observable,
@@
-  getFilters = (projectId: string) => this.filtersMap[projectId];
+  getFilters = (projectId: string) => this.filtersMap.get(projectId);
@@
-  updateFilters = (projectId: string, filters: Partial<IMemberFilters>) => {
-    const current = this.filtersMap[projectId] ?? {};
-    this.filtersMap[projectId] = { ...current, ...filters };
-  };
+  updateFilters = (projectId: string, filters: Partial<IMemberFilters>) => {
+    const current = this.filtersMap.get(projectId) ?? {};
+    this.filtersMap.set(projectId, { ...current, ...filters });
+  };

Note: update call sites that access filtersMap[projectId] to use get(projectId).

Also applies to: 59-61


53-56: Role sorting likely misrepresents hierarchy.

sortProjectMembers currently sorts roles using String(role). That’s alphabetical, not by privilege (Guest < Member < Admin). The UI labels imply hierarchy sorting.

Would you like a patch in utils.ts to map roles to a numeric rank (e.g., ROLE_RANK) and sort by that rank?

apps/web/core/store/member/workspace/workspace-member-filters.store.ts (1)

68-71: Add a small utility for resetting filters.

Handy for “Clear all” UX and when navigating between workspaces.

   updateFilters = (filters: Partial<IMemberFilters>) => {
     this.filters = { ...this.filters, ...filters };
   };
+
+  resetFilters = () => {
+    this.filters = {};
+  };
apps/web/core/store/member/project/base-project-member.store.ts (3)

120-131: Reuse the filters store API instead of re-implementing sorting.

Delegate to filters.getFilteredMemberIds for consistency and caching.

-    // Access the filters directly to ensure MobX tracking
-    const currentFilters = this.filters.filtersMap[projectId];
-
-    // Apply filters and sorting directly here to ensure MobX tracking
-    const sortedMembers = sortProjectMembers(
-      members,
-      this.memberRoot?.memberMap || {},
-      (member) => member.member,
-      currentFilters
-    );
-
-    return sortedMembers.map((member) => member.member);
+    // Ensure MobX tracks filters access via store API
+    return this.filters.getFilteredMemberIds(
+      members,
+      this.memberRoot?.memberMap || {},
+      (member) => member.member,
+      projectId
+    );

231-242: Avoid O(n) lookups when checking membership against filtered IDs.

If this path is called per-row, convert to a Set for O(1) membership checks.

-    const filteredMemberIds = this.filters.getFilteredMemberIds(
+    const filteredMemberIds = this.filters.getFilteredMemberIds(
       allMembers,
       this.memberRoot?.memberMap || {},
       (member) => member.member,
       projectId
     );
-
-    // Return null if this user doesn't pass the filters
-    if (!filteredMemberIds.includes(userId)) return null;
+    const filteredSet = new Set(filteredMemberIds);
+    if (!filteredSet.has(userId)) return null;

120-123: Don’t reach into filtersMap directly.

Use the store accessor (e.g., getFilters(projectId)) to decouple callers from the internal data structure.

-    const currentFilters = this.filters.filtersMap[projectId];
+    const currentFilters = this.filters.getFilters(projectId);

Note: This becomes moot if you adopt the refactor to reuse getFilteredMemberIds.

packages/constants/src/members.ts (2)

70-79: Confirm role sorting semantics match UI labels.

Labels imply Guest→Admin for ascending, but current sort (via utils.ts) is lexicographic on String(role). If roles are enums/strings, ordering may not reflect hierarchy.

Consider introducing a ROLE_RANK map and using it in sort utils for the “role” property.


15-23: Type naming is broader than “Project”.

These headers are used for workspace lists too. Consider renaming to IMemberDisplayProperties to reflect reuse and avoid confusion.

-export interface IProjectMemberDisplayProperties {
+export interface IMemberDisplayProperties {
@@
-export const MEMBER_PROPERTY_DETAILS: {
-  [key in keyof IProjectMemberDisplayProperties]: {
+export const MEMBER_PROPERTY_DETAILS: {
+  [key in keyof IMemberDisplayProperties]: {
apps/web/core/store/member/utils.ts (4)

11-26: Tighten the type of field returned by parseOrderKey.

Constrain field to the non-prefixed order keys to prevent accidental misuse downstream.

Apply:

-// Helper function to parse order key and direction
-export const parseOrderKey = (orderKey?: TMemberOrderByOptions): { field: string; direction: "asc" | "desc" } => {
+// Helper function to parse order key and direction
+type TOrderField = TMemberOrderByOptions extends `-${infer R}` ? R : TMemberOrderByOptions;
+export const parseOrderKey = (
+  orderKey?: TMemberOrderByOptions
+): { field: TOrderField; direction: "asc" | "desc" } => {
   // Default to sorting by display_name in ascending order when no order key is provided
   if (!orderKey) {
     return {
-      field: "display_name",
+      field: "display_name",
       direction: "asc",
     };
   }

   const isDescending = orderKey.startsWith("-");
-  const field = isDescending ? orderKey.slice(1) : orderKey;
+  const field = (isDescending ? orderKey.slice(1) : orderKey) as TOrderField;
   return {
     field,
     direction: isDescending ? "desc" : "asc",
   };
 };

110-113: Optional: use Intl.Collator for consistent, faster string ordering.

Collator with sensitivity: "base" handles accents/case gracefully; you already lowercase most fields, so this is polish.

-      const aStr = String(aValue);
-      const bStr = String(bValue);
-      comparison = aStr.localeCompare(bStr);
+      const aStr = String(aValue);
+      const bStr = String(bValue);
+      comparison = new Intl.Collator(undefined, { sensitivity: "base", numeric: true }).compare(aStr, bStr);

115-116: Optional: add a stable tie-breaker.

When values are equal, sort by the member key to keep output deterministic across engines.

-    return direction === "desc" ? -comparison : comparison;
+    if (comparison === 0) {
+      // stable tie-breaker
+      comparison = getMemberKey(a).localeCompare(getMemberKey(b));
+    }
+    return direction === "desc" ? -comparison : comparison;

50-73: Case-insensitive role filters (optional).

If role labels from UI can vary in case, normalize both sides to lower once for O(1) lookups.

-  return members.filter((member) => {
-    const memberRole = String((member.role ?? member.original_role) ?? "");
-    return roleFilters.includes(memberRole);
-  });
+  const roleSet = new Set(roleFilters.map((r) => r.toString().toLowerCase()));
+  return members.filter((member) => {
+    const memberRole = String((member.role ?? member.original_role) ?? "").toLowerCase();
+    return roleSet.has(memberRole);
+  });

And similarly in sortWorkspaceMembers’ filter path.

Also applies to: 119-167

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c3e7cfd and 3983fc1.

📒 Files selected for processing (19)
  • apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/members/page.tsx (4 hunks)
  • apps/web/ce/components/projects/settings/useProjectColumns.tsx (5 hunks)
  • apps/web/ce/components/workspace/settings/useMemberColumns.tsx (5 hunks)
  • apps/web/ce/store/member/project-member.store.ts (1 hunks)
  • apps/web/core/components/project/dropdowns/filters/member-list.tsx (1 hunks)
  • apps/web/core/components/project/member-header-column.tsx (1 hunks)
  • apps/web/core/components/project/member-list-item.tsx (1 hunks)
  • apps/web/core/components/project/member-list.tsx (4 hunks)
  • apps/web/core/components/workspace/settings/members-list.tsx (2 hunks)
  • apps/web/core/store/issue/root.store.ts (1 hunks)
  • apps/web/core/store/member/index.ts (1 hunks)
  • apps/web/core/store/member/project/base-project-member.store.ts (5 hunks)
  • apps/web/core/store/member/project/project-member-filters.store.ts (1 hunks)
  • apps/web/core/store/member/utils.ts (1 hunks)
  • apps/web/core/store/member/workspace/workspace-member-filters.store.ts (1 hunks)
  • apps/web/core/store/member/workspace/workspace-member.store.ts (5 hunks)
  • packages/constants/src/index.ts (1 hunks)
  • packages/constants/src/members.ts (1 hunks)
  • packages/i18n/src/locales/en/translations.json (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (11)
apps/web/core/components/project/member-header-column.tsx (3)
packages/constants/src/members.ts (3)
  • IProjectMemberDisplayProperties (15-21)
  • MEMBER_PROPERTY_DETAILS (23-79)
  • TMemberOrderByOptions (3-13)
apps/web/core/store/member/utils.ts (1)
  • IMemberFilters (5-8)
packages/i18n/src/hooks/use-translation.ts (1)
  • useTranslation (23-35)
apps/web/core/store/member/project/base-project-member.store.ts (2)
apps/web/core/store/member/project/project-member-filters.store.ts (2)
  • IProjectMemberFiltersStore (8-21)
  • ProjectMemberFiltersStore (23-69)
apps/web/core/store/member/utils.ts (1)
  • sortProjectMembers (120-144)
apps/web/core/store/member/project/project-member-filters.store.ts (3)
apps/web/core/store/member/utils.ts (2)
  • IMemberFilters (5-8)
  • sortProjectMembers (120-144)
packages/types/src/project/projects.ts (1)
  • TProjectMembership (93-107)
packages/types/src/users.ts (1)
  • IUserLite (20-29)
apps/web/core/components/workspace/settings/members-list.tsx (1)
apps/space/core/store/publish/publish.store.ts (1)
  • workspaceSlug (93-95)
apps/web/core/components/project/member-list.tsx (4)
apps/space/core/hooks/store/use-member.ts (1)
  • useMember (7-11)
apps/web/core/store/member/project/base-project-member.store.ts (1)
  • projectMemberIds (113-132)
apps/web/core/components/project/dropdowns/filters/member-list.tsx (1)
  • MemberListFiltersDropdown (83-106)
packages/constants/src/event-tracker/core.ts (1)
  • MEMBER_TRACKER_ELEMENTS (249-258)
apps/web/core/store/member/utils.ts (3)
packages/constants/src/members.ts (1)
  • TMemberOrderByOptions (3-13)
packages/types/src/users.ts (1)
  • IUserLite (20-29)
packages/types/src/project/projects.ts (1)
  • TProjectMembership (93-107)
apps/web/core/store/member/workspace/workspace-member-filters.store.ts (3)
apps/web/core/store/member/workspace/workspace-member.store.ts (1)
  • IWorkspaceMembership (19-24)
apps/web/core/store/member/utils.ts (2)
  • IMemberFilters (5-8)
  • sortWorkspaceMembers (146-167)
packages/types/src/users.ts (1)
  • IUserLite (20-29)
apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/members/page.tsx (3)
apps/web/core/components/project/dropdowns/filters/member-list.tsx (1)
  • MemberListFiltersDropdown (83-106)
packages/constants/src/event-tracker/core.ts (1)
  • MEMBER_TRACKER_ELEMENTS (249-258)
apps/web/core/components/workspace/settings/members-list.tsx (1)
  • WorkspaceMembersList (19-97)
apps/web/core/store/member/workspace/workspace-member.store.ts (1)
apps/web/core/store/member/workspace/workspace-member-filters.store.ts (2)
  • IWorkspaceMemberFiltersStore (17-28)
  • WorkspaceMemberFiltersStore (30-71)
apps/web/ce/components/projects/settings/useProjectColumns.tsx (6)
packages/types/src/project/projects.ts (1)
  • TProjectMembership (93-107)
packages/types/src/workspace.ts (1)
  • IWorkspaceMember (74-87)
apps/space/core/hooks/store/use-user.ts (1)
  • useUser (7-11)
apps/space/core/hooks/store/use-member.ts (1)
  • useMember (7-11)
apps/web/core/store/member/utils.ts (1)
  • IMemberFilters (5-8)
apps/web/core/components/project/member-header-column.tsx (1)
  • MemberHeaderColumn (18-114)
apps/web/ce/components/workspace/settings/useMemberColumns.tsx (3)
apps/space/core/hooks/store/use-member.ts (1)
  • useMember (7-11)
apps/web/core/store/member/utils.ts (1)
  • IMemberFilters (5-8)
apps/web/core/components/project/member-header-column.tsx (1)
  • MemberHeaderColumn (18-114)
⏰ 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). (2)
  • GitHub Check: Analyze (javascript)
  • GitHub Check: Build and lint web apps
🔇 Additional comments (17)
apps/web/core/components/project/dropdowns/filters/member-list.tsx (1)

48-50: Confirm FilterHeader prop semantics.

If “isPreviewEnabled” means “collapsed preview,” toggling with isExpanded may be inverted.

Please verify the prop’s intended meaning and adjust the boolean or label if necessary.

apps/web/core/components/project/member-list-item.tsx (1)

16-16: Import path update approved — no lingering old imports found.
Searched for '@/store/member/base-project-member.store' (and variants): none found. Type is defined at apps/web/core/store/member/project/base-project-member.store.ts and imports are updated in apps/web/core/components/project/member-list-item.tsx and apps/web/ce/store/member/project-member.store.ts.

packages/constants/src/index.ts (1)

10-10: Good: re-exporting members.

Keeps constants discoverable via package entrypoint.

apps/web/ce/store/member/project-member.store.ts (1)

8-8: Good: import path realigned with new store location.

No runtime impact expected.

apps/web/core/store/issue/root.store.ts (1)

32-32: Good: workspace membership type import path updated.

Matches the new workspace store layout.

apps/web/core/store/member/index.ts (1)

9-9: Good: workspace store import moved under workspace/.

Keeps structure consistent with the refactor.

apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/members/page.tsx (3)

92-101: Role filter toggle logic looks solid.

Toggling roles and clearing via undefined is correct and MobX-friendly.


143-147: Filters dropdown integration: LGTM.

Wiring appliedFilters and handleUpdate is correct.


161-161: Search likely ignores applied role filters in workspace view.

WorkspaceMembersList currently searches over unfiltered workspaceMemberIds, so typing a query can surface members outside the selected roles. Please ensure search is applied on top of the filtered set. See suggested store fix in workspace-member.store.ts for a minimal change.

apps/web/core/store/member/workspace/workspace-member.store.ts (4)

16-18: Drop “.ts” from type-only import to avoid resolution inconsistencies.

Some TS module resolutions treat extensions differently and this is inconsistent with neighboring imports.

-import type { IMemberRootStore } from "../index.ts";
+import type { IMemberRootStore } from "../index";

136-154: Filtered member ids computation: LGTM.

Bots and inactive filtered first, then delegate to filters store for sorting/filtering. Nice separation.


123-134: Default ordering parity.

workspaceMemberIds prioritizes current user then alpha; getFilteredWorkspaceMemberIds delegates to sortWorkspaceMembers which may not. Please confirm the utility preserves this default when no explicit order_by is set.

Also applies to: 136-154


91-93: Filters lifetime across workspaces.

filtersStore is singleton for the store; consider resetting filters when workspaceSlug changes to avoid leaking filters between workspaces.

apps/web/core/components/project/member-header-column.tsx (1)

37-113: Header sorting UX: solid and self-contained.

Clear indicator, toggle handlers, and “clear sorting” action are correct.

apps/web/core/store/member/workspace/workspace-member-filters.store.ts (1)

30-41: Workspace filters store looks solid.

Observable state, action updates, and computedFn usage are correct and consistent with the project store.

Also applies to: 49-62, 68-71

apps/web/core/store/member/utils.ts (2)

1-9: Good extraction and API surface.

Clear, reusable utilities with a thin IMemberFilters contract. This will keep project/workspace pipelines consistent.


1-168: Follow-ups — add tests & verify role-token mapping.

  • Add unit tests for: role filter handling falsey / 0-like values, case-insensitive role sort, missing memberDetails, and missing/invalid joining_date.
  • Verify UI role tokens match server enums: EUserPermissions / EUserProjectRoles are numeric (ADMIN=20, MEMBER=15, GUEST=5). Check packages/types/src/enums.ts, packages/types/src/project/projects.ts, packages/constants/src/user.ts and apps/web/core/store/member/utils.ts (String(member.role || ...)). Ensure filters.roles use the same token format (numeric strings or mapped names) or add explicit mapping before filtering/sorting.

@vamsikrishnamathala vamsikrishnamathala marked this pull request as draft September 12, 2025 12:01
@vamsikrishnamathala vamsikrishnamathala marked this pull request as ready for review September 22, 2025 13:58
cursor[bot]

This comment was marked as outdated.

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 (7)
apps/web/ce/components/projects/settings/useProjectColumns.tsx (4)

39-46: Remove unnecessary string conversions/casts.

workspaceSlug/projectId are already strings here; drop .toString() and redundant as string casts.

Apply:

-  const isAdmin = allowPermissions(
-    [EUserPermissions.ADMIN],
-    EUserPermissionsLevel.PROJECT,
-    workspaceSlug.toString(),
-    projectId.toString()
-  );
+  const isAdmin = allowPermissions(
+    [EUserPermissions.ADMIN],
+    EUserPermissionsLevel.PROJECT,
+    workspaceSlug,
+    projectId
+  );

-  const currentProjectRole =
-    getProjectRoleByWorkspaceSlugAndProjectId(workspaceSlug.toString(), projectId.toString()) ?? EUserPermissions.GUEST;
+  const currentProjectRole =
+    getProjectRoleByWorkspaceSlugAndProjectId(workspaceSlug, projectId) ?? EUserPermissions.GUEST;

-          workspaceSlug={workspaceSlug as string}
+          workspaceSlug={workspaceSlug}

-          projectId={projectId as string}
+          projectId={projectId}
-          workspaceSlug={workspaceSlug as string}
+          workspaceSlug={workspaceSlug}

Also applies to: 70-70, 115-117


99-99: Email cell: add truncation and a safe fallback.

Prevents layout break on long emails and avoids rendering empty cells.

-      tdRender: (rowData: RowData) => <div className="w-48 text-custom-text-200">{rowData.member.email}</div>,
+      tdRender: (rowData: RowData) => (
+        <div className="w-48 truncate text-custom-text-200" title={rowData.member.email ?? ""}>
+          {rowData.member.email ?? "—"}
+        </div>
+      ),

58-59: Consider i18n for content labels.

Workspace columns use t(...). For consistency and a11y (e.g., accessible headers), consider localizing these content strings too.

Also applies to: 79-80, 91-92, 103-104, 121-122


55-132: Optional: memoize columns.

If the consumer re-renders frequently, wrap columns in useMemo to reduce churn; deps: [displayFilters, handleDisplayFilterUpdate, workspaceSlug, isAdmin, currentUser, currentProjectRole].

apps/web/ce/components/workspace/settings/useMemberColumns.tsx (1)

81-81: Email cell: add safe fallback and tooltip.

Keeps layout stable and improves readability on long emails.

-      tdRender: (rowData: RowData) => <div className="w-48 truncate">{rowData.member.email}</div>,
+      tdRender: (rowData: RowData) => (
+        <div className="w-48 truncate" title={rowData.member.email ?? ""}>
+          {rowData.member.email ?? "—"}
+        </div>
+      ),
apps/web/core/store/member/utils.ts (2)

108-112: Optional: use Intl.Collator for locale-aware, faster string compares.

-      const aStr = String(aValue);
-      const bStr = String(bValue);
-      comparison = aStr.localeCompare(bStr);
+      const aStr = String(aValue);
+      const bStr = String(bValue);
+      const collator = new Intl.Collator(undefined, { sensitivity: "base" });
+      comparison = collator.compare(aStr, bStr);

20-26: Guard unknown fields from parseOrderKey (optional).

If an invalid field slips in, you’ll sort by empty strings. Consider validating against the allowed set and defaulting to display_name.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3983fc1 and 36a399e.

📒 Files selected for processing (6)
  • apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/members/page.tsx (4 hunks)
  • apps/web/ce/components/projects/settings/useProjectColumns.tsx (5 hunks)
  • apps/web/ce/components/workspace/settings/useMemberColumns.tsx (4 hunks)
  • apps/web/core/store/member/project/project-member-filters.store.ts (1 hunks)
  • apps/web/core/store/member/utils.ts (1 hunks)
  • packages/constants/src/index.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/constants/src/index.ts
  • apps/web/core/store/member/project/project-member-filters.store.ts
🧰 Additional context used
🧬 Code graph analysis (4)
apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/members/page.tsx (2)
apps/web/core/components/project/dropdowns/filters/member-list.tsx (1)
  • MemberListFiltersDropdown (83-106)
packages/constants/src/event-tracker/core.ts (1)
  • MEMBER_TRACKER_ELEMENTS (249-258)
apps/web/core/store/member/utils.ts (3)
packages/constants/src/members.ts (1)
  • TMemberOrderByOptions (3-13)
packages/types/src/users.ts (1)
  • IUserLite (20-29)
packages/types/src/project/projects.ts (1)
  • TProjectMembership (93-107)
apps/web/ce/components/workspace/settings/useMemberColumns.tsx (3)
apps/space/core/hooks/store/use-member.ts (1)
  • useMember (7-11)
apps/web/core/store/member/utils.ts (1)
  • IMemberFilters (5-8)
apps/web/core/components/project/member-header-column.tsx (1)
  • MemberHeaderColumn (18-114)
apps/web/ce/components/projects/settings/useProjectColumns.tsx (5)
packages/types/src/project/projects.ts (1)
  • TProjectMembership (93-107)
packages/types/src/workspace.ts (1)
  • IWorkspaceMember (74-87)
apps/space/core/hooks/store/use-member.ts (1)
  • useMember (7-11)
apps/web/core/store/member/utils.ts (1)
  • IMemberFilters (5-8)
apps/web/core/components/project/member-header-column.tsx (1)
  • MemberHeaderColumn (18-114)
⏰ 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). (3)
  • GitHub Check: Cursor Bugbot
  • GitHub Check: Build and lint web apps
  • GitHub Check: Analyze (javascript)
🔇 Additional comments (10)
apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/members/page.tsx (6)

23-23: LGTM! Proper import for the new filter component.

The import follows the existing project structure and the component is being used correctly.


92-101: LGTM! Well-implemented role filter handler.

The handler correctly:

  • Retrieves current filters and roles
  • Implements toggle logic (add/remove role)
  • Cleans up by setting undefined when no roles are selected
  • Updates the store appropriately

105-105: LGTM! Proper derived state for filter dropdown.

The appliedRoleFilters is correctly derived from the filtersStore and provides a safe fallback to an empty array.


132-159: LGTM! Well-structured header with comprehensive filtering controls.

The header redesign successfully integrates:

  • Search input with proper styling and accessibility
  • Role filter dropdown with correct props
  • Conditional admin actions (Add Member button)
  • Billing actions button

The layout is clean and maintains existing authorization patterns.


45-45: Verify the filtersStore is properly exposed by useMember hook.

The destructuring adds filtersStore from the workspace object returned by useMember(). Ensure this property is available and properly typed in the hook's return value.


143-147: Verify MemberListFiltersDropdown accepts workspace memberType.

Ensure the MemberListFiltersDropdown component properly handles the memberType="workspace" prop and that workspace-specific roles are supported.

apps/web/ce/components/projects/settings/useProjectColumns.tsx (1)

33-38: Solid wiring of header-driven sorting/filters.

The store integration, handler plumbing, and MemberHeaderColumn usage per column look correct.

Also applies to: 48-53, 60-66, 80-86, 92-99, 104-110, 124-130, 133-139

apps/web/ce/components/workspace/settings/useMemberColumns.tsx (1)

20-24: Workspace header filters/sorting integration looks good.

Nice alignment with project columns and consistent handler usage. Switching to renderFormattedDate addresses the prior invalid-date concern.

Please confirm renderFormattedDate gracefully handles undefined/invalid dates (returns a placeholder rather than “Invalid Date”).

Also applies to: 30-34, 41-46, 62-67, 75-80, 88-93, 109-115

apps/web/core/store/member/utils.ts (2)

5-8: Filter type + order parsing + normalized sort keys look correct.

Good defaults, case-insensitive strings, and NaN date sentinel from getMemberSortKey(joining_date).

Also applies to: 10-26, 28-47


92-92: Deterministic ordering when details are missing.

Returning 0 whenever either side is missing yields unstable results; push missing to the end consistently.

-    if (!aMemberDetails || !bMemberDetails) return 0;
+    if (!aMemberDetails && !bMemberDetails) return 0;
+    if (!aMemberDetails) return 1;  // a after b
+    if (!bMemberDetails) return -1; // a before b

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 (5)
apps/web/core/store/member/workspace/workspace-member.store.ts (5)

15-17: Avoid .ts extension in import path unless using NodeNext ESM.

Using ../index.ts can break under common TS/Next setups that expect extensionless or .js-suffixed imports at runtime. Prefer ../index for consistency with the rest of the codebase unless you’ve standardized on NodeNext.

Apply this diff:

-import type { IMemberRootStore } from "../index.ts";
+import type { IMemberRootStore } from "../index";

30-31: Consider marking filtersStore as readonly in the interface.

Conveys intent and prevents accidental reassignment.

Apply this diff:

-  filtersStore: IWorkspaceMemberFiltersStore;
+  readonly filtersStore: IWorkspaceMemberFiltersStore;

65-66: Make the class property filtersStore readonly as well.

Keeps implementation aligned with the interface and avoids reassignment.

Apply this diff:

-  filtersStore: IWorkspaceMemberFiltersStore;
+  readonly filtersStore: IWorkspaceMemberFiltersStore;

136-154: Minor tidy + DRY: inline return and (optionally) extract common active/non‑bot filter.

  • Inline return removes temporary variables.
  • Optional: extract a helper (e.g., getActiveHumanMembers(workspaceSlug)) to avoid duplicating the active/non‑bot filter logic here and in getWorkspaceMemberIds.

Apply this diff to simplify:

-  getFilteredWorkspaceMemberIds = computedFn((workspaceSlug: string) => {
-    let members = Object.values(this.workspaceMemberMap?.[workspaceSlug] ?? {});
-    //filter out bots and inactive members
-    members = members.filter((m) => m.is_active && !this.memberRoot?.memberMap?.[m.member]?.is_bot);
-
-    // Use filters store to get filtered member ids
-    const memberIds = this.filtersStore.getFilteredMemberIds(
-      members,
-      this.memberRoot?.memberMap || {},
-      (member) => member.member
-    );
-
-    return memberIds;
-  });
+  getFilteredWorkspaceMemberIds = computedFn((workspaceSlug: string) => {
+    const members = Object.values(this.workspaceMemberMap?.[workspaceSlug] ?? {}).filter(
+      (m) => m.is_active && !this.memberRoot?.memberMap?.[m.member]?.is_bot
+    );
+    return this.filtersStore.getFilteredMemberIds(members, this.memberRoot?.memberMap || {}, (member) => member.member);
+  });

Optional helper outside this range (for reuse):

private getActiveHumanMembers = (workspaceSlug: string): IWorkspaceMembership[] =>
  Object.values(this.workspaceMemberMap?.[workspaceSlug] ?? {}).filter(
    (m) => m.is_active && !this.memberRoot?.memberMap?.[m.member]?.is_bot
  );

162-166: Remove redundant null check.

getFilteredWorkspaceMemberIds always returns an array; the null check is dead code.

Apply this diff:

-    const filteredMemberIds = this.getFilteredWorkspaceMemberIds(workspaceSlug);
-    if (!filteredMemberIds) return null;
+    const filteredMemberIds = this.getFilteredWorkspaceMemberIds(workspaceSlug);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 36a399e and 483c482.

📒 Files selected for processing (1)
  • apps/web/core/store/member/workspace/workspace-member.store.ts (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
apps/web/core/store/member/workspace/workspace-member.store.ts (1)
apps/web/core/store/member/workspace/workspace-member-filters.store.ts (2)
  • IWorkspaceMemberFiltersStore (17-28)
  • WorkspaceMemberFiltersStore (30-71)
⏰ 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). (3)
  • GitHub Check: Build and lint web apps
  • GitHub Check: Cursor Bugbot
  • GitHub Check: Analyze (javascript)
🔇 Additional comments (2)
apps/web/core/store/member/workspace/workspace-member.store.ts (2)

38-38: LGTM: New API to expose filtered member IDs.

Signature and return type look good and align with usage below.


91-93: Confirm filter scoping: global vs per‑workspace.

filtersStore holds a single filters object shared across workspaces. Is that intentional? If filters should be per‑workspace, consider keying by workspaceSlug or resetting on workspace change.

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 (5)
apps/web/ce/components/projects/settings/useProjectColumns.tsx (5)

58-64: Header sorting integration looks good; minor a11y check.

If MemberHeaderColumn sets customButtonTabIndex to -1 (as in core), the header menu isn’t tabbable. Consider 0 to keep it keyboard-accessible.

To adjust (in member-header-column.tsx):

// change
customButtonTabIndex={-1}
// to
customButtonTabIndex={0}

79-85: Fallback for empty display name.

Show a placeholder when display_name is missing.

-      tdRender: (rowData: RowData) => <div className="w-32">{rowData.member.display_name}</div>,
+      tdRender: (rowData: RowData) => (
+        <div className="w-32">{rowData.member.display_name || "-"}</div>
+      ),

88-98: Email cell: add truncation and tooltip for long addresses.

Improves layout and usability without changing behavior.

-      tdRender: (rowData: RowData) => <div className="w-48 text-custom-text-200">{rowData.member.email}</div>,
+      tdRender: (rowData: RowData) => (
+        <div className="w-48 truncate text-custom-text-200" title={rowData.member.email || ""}>
+          {rowData.member.email || "-"}
+        </div>
+      ),

121-129: Joining date: handle null/undefined gracefully.

Guard for missing value to avoid rendering “Invalid date” if upstream isn’t set.

-      tdRender: (rowData: RowData) => <div>{renderFormattedDate(rowData?.member?.joining_date)}</div>,
+      tdRender: (rowData: RowData) => (
+        <div>
+          {rowData?.member?.joining_date ? renderFormattedDate(rowData.member.joining_date) : "-"}
+        </div>
+      ),

49-51: Stabilize handler with useCallback (optional).

Prevents unnecessary re-renders of memoized children that depend on this prop.

-import { useState } from "react";
+import { useCallback, useState } from "react";
@@
-  const handleDisplayFilterUpdate = (filters: Partial<IMemberFilters>) => {
-    updateFilters(projectId, filters);
-  };
+  const handleDisplayFilterUpdate = useCallback(
+    (filters: Partial<IMemberFilters>) => {
+      updateFilters(projectId, filters);
+    },
+    [projectId, updateFilters]
+  );
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 483c482 and 383f83d.

📒 Files selected for processing (2)
  • apps/web/ce/components/projects/settings/useProjectColumns.tsx (5 hunks)
  • apps/web/core/components/pages/version/editor.tsx (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • apps/web/core/components/pages/version/editor.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
apps/web/ce/components/projects/settings/useProjectColumns.tsx (3)
apps/space/core/hooks/store/use-member.ts (1)
  • useMember (7-11)
apps/web/core/store/member/utils.ts (1)
  • IMemberFilters (5-8)
apps/web/core/components/project/member-header-column.tsx (1)
  • MemberHeaderColumn (18-114)
⏰ 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). (3)
  • GitHub Check: Build and lint web apps
  • GitHub Check: Cursor Bugbot
  • GitHub Check: Analyze (javascript)
🔇 Additional comments (3)
apps/web/ce/components/projects/settings/useProjectColumns.tsx (3)

7-12: Verify CE build resolves cross-app aliases (MemberHeaderColumn, IMemberFilters)

Both symbols live in core: apps/web/core/components/project/member-header-column.tsx and apps/web/core/store/member/utils.ts — confirm the CE app's path alias/bundler (tsconfig/webpack/vite) maps '@' to core or re-export/move these into a shared package so CE can resolve them.


31-35: Store API confirmed — updateFilters requires a filters arg

getFilters(projectId: string): IMemberFilters | undefined; updateFilters(projectId: string, filters: Partial): void — filters is required (not optional). Defined in apps/web/core/store/member/project/project-member-filters.store.ts.

Likely an incorrect or invalid review comment.


131-137: Resolved — verified single consumer; no downstream changes required.

Only usage found: apps/web/core/components/project/member-list-item.tsx:37 — it destructures columns, removeMemberModal, setRemoveMemberModal and will ignore the newly added properties, so this API expansion is non-breaking.

@pushya22 pushya22 merged commit 586a7a4 into preview Sep 24, 2025
7 of 8 checks passed
@pushya22 pushya22 deleted the feat-project_member_filters branch September 24, 2025 12:15
@coderabbitai coderabbitai bot mentioned this pull request Sep 29, 2025
6 tasks
yarikoptic pushed a commit to yarikoptic/plane that referenced this pull request Oct 1, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🌟enhancement New feature or request ready to merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants