Skip to content

Conversation

vamsikrishnamathala
Copy link
Member

@vamsikrishnamathala vamsikrishnamathala commented Sep 23, 2025

Description

This update adds suspended users to the workspace members list. In dropdowns, suspended users are also included but positioned at the bottom for better clarity and organization.

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)

Test Scenarios

References

Summary by CodeRabbit

  • New Features

    • Role-based filters for project and workspace member lists (workspace adds "Suspended"); filter dropdown next to search.
    • Sortable member table headers for name, display name, email, joining date, and role.
  • Enhancements

    • Combined header: search, filters, and conditional Add Member button (opens invite modal for admins).
    • Member lists now prioritize active members.
    • New translations for member attributes.
  • UI

    • Suspended member indicators: icon, pill badge, muted styling, and disabled actions.
    • Compact Pill (XS) size added.

Copy link
Contributor

coderabbitai bot commented Sep 23, 2025

Walkthrough

Adds observable per-project and per-workspace member filter stores, role-based filtering UI (dropdown + header controls), sortable member header component, suspended-member UI/icons, sorting/filtering utilities, and integrates filters into member list rendering and stores.

Changes

Cohort / File(s) Summary
Workspace members page wiring
apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/members/page.tsx
Add MemberListFiltersDropdown, wire workspace filtersStore, implement handleRoleFilterUpdate and appliedRoleFilters, integrate Add Member button/invite modal with permissions.
Project filters UI & metadata
apps/web/core/components/project/dropdowns/filters/member-list.tsx, apps/web/core/components/project/member-header-column.tsx, packages/constants/src/members.ts, packages/constants/src/index.ts, packages/i18n/src/locales/en/translations.json
New MemberListFilters / MemberListFiltersDropdown components, MemberHeaderColumn sortable header, new MEMBER_PROPERTY_DETAILS constants, i18n keys, and re-export of members constants.
Project member list & columns integration
apps/web/core/components/project/member-list.tsx, apps/web/ce/components/projects/settings/useProjectColumns.tsx, apps/web/core/components/project/member-list-item.tsx
Use getFilteredProjectMemberDetails and project filters, add role filter handlers, expose displayFilters and handleDisplayFilterUpdate in project columns hook, update import paths.
Workspace members list & columns
apps/web/core/components/workspace/settings/members-list.tsx, apps/web/ce/components/workspace/settings/useMemberColumns.tsx, apps/web/core/components/workspace/settings/member-columns.tsx, apps/web/core/components/dropdowns/member/member-options.tsx
Add getFilteredWorkspaceMemberIds and filtersStore, prefer filtered IDs in search, add suspended-state visuals/disabled dropdown options, header-based filtering and conditional styling/behavior for suspended members.
Stores: filter stores and utils
apps/web/core/store/member/project/project-member-filters.store.ts, apps/web/core/store/member/project/base-project-member.store.ts, 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/utils.ts
New MobX filter stores for projects/workspaces, integrate stores into member stores, add getFiltered* getters, isUserSuspended, and utility functions for parsing order_by, role filtering (including suspended), and sorting.
Store path/type updates
apps/web/ce/store/member/project-member.store.ts, apps/web/core/store/member/index.ts, apps/web/core/store/issue/root.store.ts
Adjust import paths for relocated project/workspace store modules and types.
UI primitives: icons & pill
packages/propel/src/icons/suspended-user.tsx, packages/propel/src/icons/index.ts, packages/propel/src/pill/pill.tsx, packages/propel/src/pill/index.ts
Add SuspendedUserIcon and re-export; add XS size for Pill and re-export size/variant enums.
Minor refactor
apps/web/core/components/pages/version/editor.tsx
Reordered imports only (no behavior change).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant UI as Member List Header
  participant Dropdown as MemberListFiltersDropdown
  participant Filters as FiltersStore
  participant Store as MemberStore
  participant View as Member Table

  User->>UI: open filters / toggle role or sort
  UI->>Dropdown: render with appliedFilters
  Dropdown->>Filters: updateFilters(projectOrWorkspaceId, partial)
  Filters-->>Store: observable filters updated
  Store->>Store: compute getFiltered*MemberIds(...) via utils (filter + sort)
  Store-->>View: provide filtered member IDs/details
  View-->>User: render updated list (suspended styling applied)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

🌐frontend, 🌟improvement, requires approval

Suggested reviewers

  • prateekshourya29
  • sriramveeraghanta

Poem

In my burrow I hop through lines and strands,
Toggling roles with soft, swift hands.
Suspended friends get a gentle sign,
Sorts and filters make the list aligned.
A carrot for teamwork — hops and stands! 🥕🐰

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title Check ✅ Passed The title "[WEB-4882]feat: suspended users" is concise and directly summarizes the primary change (adding suspended-user support) across the codebase, using the issue key and a clear feature tag without extraneous detail.
Description Check ✅ Passed The PR description gives a clear summary and correctly marks the change type as an Improvement, satisfying the template's core sections, but it omits Test Scenarios, Screenshots/Media, and does not call out important scope details such as public/store API changes and any migration/backward-compatibility notes introduced by the diffs.
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-suspended_users

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

makeplane bot commented Sep 23, 2025

Linked to Plane Work Item(s)

This comment was auto-generated by Plane

@vamsikrishnamathala vamsikrishnamathala self-assigned this Sep 23, 2025
@vamsikrishnamathala vamsikrishnamathala marked this pull request as ready for review September 23, 2025 13:59
Copy link

cursor bot commented Sep 23, 2025

You have run out of free Bugbot PR reviews for this billing cycle. This will reset on October 20.

To receive reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

@vamsikrishnamathala vamsikrishnamathala added the 🌟enhancement New feature or request label Sep 23, 2025
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: 7

Caution

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

⚠️ Outside diff range comments (1)
apps/web/core/components/project/member-list.tsx (1)

42-45: Guard against undefined display_name (runtime crash).

display_name can be null/undefined; toLowerCase() would throw.

-    const displayName = memberDetails?.member.display_name.toLowerCase();
+    const displayName = memberDetails?.member.display_name?.toLowerCase() ?? "";
🧹 Nitpick comments (22)
packages/propel/src/icons/suspended-user.tsx (1)

5-14: Add basic accessibility attributes to SVG.

Mark presentational by default to keep it out of the a11y tree.

 export const SuspendedUserIcon: React.FC<ISvgIcons> = ({ className, ...rest }) => (
   <svg
     width="16"
     height="17"
     viewBox="0 0 16 17"
     fill="none"
     xmlns="http://www.w3.org/2000/svg"
     className={className}
+    focusable="false"
+    aria-hidden="true"
     {...rest}
   >
apps/web/core/components/project/dropdowns/filters/member-list.tsx (2)

47-51: Localize UI strings (“Filters”, “Roles”, role labels).

Replace hardcoded labels with i18n keys (e.g., t("common.filters"), t("project_members.role"), and role names). Ensures consistency and translation coverage.

Also applies to: 89-96, 22-26, 28-33


16-20: Minor typing/ergonomics: default filters to empty array.

Use string[] instead of string[] | null and default [] from the parent. Simplifies checks (includes, counts) and avoids nullish coalescing noise.

Also applies to: 41-44, 56-66, 87-88

apps/web/core/components/workspace/settings/members-list.tsx (2)

53-55: Search currently bypasses filters/order; intersect to preserve both.

When searchQuery is present, the code ignores filteredMemberIds (including order_by and role filters). Intersect search results with filteredMemberIds to keep filters and ordering intact.

Apply this diff:

-  const searchedMemberIds = searchQuery ? getSearchedWorkspaceMemberIds(searchQuery) : filteredMemberIds;
+  const searchedMemberIds = searchQuery
+    ? filteredMemberIds.filter((id) => (getSearchedWorkspaceMemberIds(searchQuery) ?? []).includes(id))
+    : filteredMemberIds;

57-63: Avoid relying on engine sort stability; partition instead.

The comparator returns 0 for ties, depending on stable sort to preserve prior ordering. Prefer explicit partitioning to keep active members first while preserving internal order.

Apply this diff:

-  const memberDetails = searchedMemberIds
-    ?.map((memberId) => getWorkspaceMemberDetails(memberId))
-    .sort((a, b) => {
-      if (a?.is_active && !b?.is_active) return -1;
-      if (!a?.is_active && b?.is_active) return 1;
-      return 0;
-    });
+  const memberDetailsUnsorted = searchedMemberIds?.map((memberId) => getWorkspaceMemberDetails(memberId)) ?? [];
+  const activeMembers = memberDetailsUnsorted.filter((m) => m?.is_active);
+  const inactiveMembers = memberDetailsUnsorted.filter((m) => !m?.is_active);
+  const memberDetails = [...activeMembers, ...inactiveMembers];
apps/web/core/components/project/member-header-column.tsx (2)

39-44: A11y: make the custom button focusable and identifiable.

customButtonTabIndex is -1, which prevents keyboard focus. Set to 0 and add a button role/label to improve accessibility.

Apply this diff:

-      customButtonTabIndex={-1}
+      customButtonTabIndex={0}
       className="!w-full"
       customButton={
-        <div className="flex w-full cursor-pointer items-center justify-between gap-1.5 py-2 text-sm text-custom-text-200 hover:text-custom-text-100">
+        <div
+          className="flex w-full cursor-pointer items-center justify-between gap-1.5 py-2 text-sm text-custom-text-200 hover:text-custom-text-100"
+          role="button"
+          aria-label={`${t(propertyDetails.i18n_title)} sorting`}
+        >

35-38: Avoid empty menus when sorting isn’t allowed.

If sorting is not allowed for a property, return a plain label instead of rendering CustomMenu.

Apply this diff:

   if (!propertyDetails) return null;
 
+  if (!propertyDetails.isSortingAllowed) {
+    return <span className="py-2 text-sm text-custom-text-200">{t(propertyDetails.i18n_title)}</span>;
+  }
+
   return (
     <CustomMenu
apps/web/core/components/workspace/settings/member-columns.tsx (3)

68-76: Align avatar placeholder size with image avatar.

The no-avatar placeholder uses h-4 w-4 while the image avatar container is h-6 w-6, causing jank. Match sizes for consistency.

Apply this diff:

-                  <span
-                    className={cn(
-                      "relative flex h-4 w-4 text-xs items-center justify-center rounded-full  capitalize text-white",
-                      isSuspended ? "bg-custom-background-80" : "bg-gray-700"
-                    )}
-                  >
+                  <span
+                    className={cn(
+                      "relative flex h-6 w-6 text-xs items-center justify-center rounded-full capitalize text-white",
+                      isSuspended ? "bg-custom-background-80" : "bg-gray-700"
+                    )}
+                  >

82-84: Avoid “undefined undefined” for missing names.

Fallback to display_name/email when first_name/last_name are absent.

Apply this diff:

-              <span className={isSuspended ? "text-custom-text-400" : ""}>
-                {first_name} {last_name}
-              </span>
+              <span className={isSuspended ? "text-custom-text-400" : ""}>
+                {first_name || last_name ? `${first_name ?? ""} ${last_name ?? ""}`.trim() : display_name ?? email}
+              </span>

134-140: Localize “Suspended”.

Use i18n instead of a hardcoded string.

Apply this diff:

+  // i18n
+  import { useTranslation } from "@plane/i18n";
@@
 export const AccountTypeColumn: React.FC<AccountTypeProps> = observer((props) => {
   const { rowData, workspaceSlug } = props;
+  const { t } = useTranslation();
@@
-      {isSuspended ? (
+      {isSuspended ? (
         <div className="w-32 flex ">
           <Pill variant={EPillVariant.DEFAULT} size={EPillSize.SM} className="border-none">
-            Suspended
+            {t("suspended")}
           </Pill>
         </div>

Confirm the correct translation key (e.g., "suspended" or a scoped key) exists in your i18n catalog.

apps/web/core/components/dropdowns/member/member-options.tsx (2)

91-111: Compute suspension once per option to avoid repeated store lookups.

Minor perf/readability improvement.

Apply this diff:

-  const options = memberIds
-    ?.map((userId) => {
-      const userDetails = getUserDetails(userId);
+  const options = memberIds
+    ?.map((userId) => {
+      const userDetails = getUserDetails(userId);
+      const userSuspended = isSuspended(userId);
       return {
         value: userId,
         query: `${userDetails?.display_name} ${userDetails?.first_name} ${userDetails?.last_name}`,
         content: (
           <div className="flex items-center gap-2">
-            <div className="w-4">
-              {isSuspended(userId) ? (
+            <div className="w-4">
+              {userSuspended ? (
                 <SuspendedUserIcon className="h-3.5 w-3.5 text-custom-text-400" />
               ) : (
                 <Avatar name={userDetails?.display_name} src={getFileURL(userDetails?.avatar_url ?? "")} />
               )}
             </div>
-            <span className={cn("flex-grow truncate", isSuspended(userId) ? "text-custom-text-400" : "")}>
+            <span className={cn("flex-grow truncate", userSuspended ? "text-custom-text-400" : "")}>
               {currentUser?.id === userId ? t("you") : userDetails?.display_name}
             </span>
           </div>
         ),
       };
     })

167-170: Localize “Suspended”.

Replace the hardcoded label with a translated string.

Apply this diff:

-                          {isSuspended(option.value) && (
-                            <Pill variant={EPillVariant.DEFAULT} size={EPillSize.XS} className="border-none">
-                              Suspended
-                            </Pill>
-                          )}
+                          {isSuspended(option.value) && (
+                            <Pill variant={EPillVariant.DEFAULT} size={EPillSize.XS} className="border-none">
+                              {t("suspended")}
+                            </Pill>
+                          )}

Confirm the right i18n key for “Suspended” in your locale files.

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

70-78: Confirm role sort semantics match UI titles.

"Guest → Admin" titles imply a specific role order. Ensure your comparator for order_by: "role"/"-role" sorts by role precedence (not lexicographic labels or enum numeric values that might not match the intended order). If needed, map roles to an explicit rank.


23-79: Consider i18n for sort titles (“A/Z”, “Old/New”, “Guest/Admin”).

These are currently hardcoded strings. If surfaced in UI, prefer i18n keys to avoid mixed-language menus.

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

132-159: Add accessible label for the search input.

Placeholder-only inputs are not accessible. Associate a visually-hidden label or use aria-label.

Example minimal tweak:

-<input
+<input
+  aria-label={t("search")}
   className="w-full max-w-[234px] border-none bg-transparent text-sm outline-none placeholder:text-custom-text-400"
   placeholder={`${t("search")}...`}
apps/web/core/store/member/workspace/workspace-member.store.ts (1)

140-142: Stale comment.

Comment says “filter out bots and inactive members” but code only filters bots. Update comment to avoid confusion.

-    //filter out bots and inactive members
-    members = members.filter((m) => !this.memberRoot?.memberMap?.[m.member]?.is_bot);
+    // filter out bots
+    members = members.filter((m) => !this.memberRoot?.memberMap?.[m.member]?.is_bot);
apps/web/core/components/project/member-list.tsx (1)

87-87: Use i18n for the search placeholder.

Align with other screens using t("search").

-              placeholder="Search"
+              placeholder={t("search")}
apps/web/core/store/member/project/base-project-member.store.ts (2)

111-132: Preserve “current user first” ordering and ensure deterministic default sort.

The previous implementation pinned the current user to the top; the new path loses that behavior. Also, if filters.order_by is unset, the current code may return backend insertion order. Recommend pinning current user first and returning IDs after a default name sort (utils can now default to display_name).

Apply this diff to pin the current user first while preserving the computed order:

     const sortedMembers = sortProjectMembers(
       members,
       this.memberRoot?.memberMap || {},
       (member) => member.member,
       currentFilters
     );
-
-    return sortedMembers.map((member) => member.member);
+    const ids = sortedMembers.map((member) => member.member);
+    const currentUserId = this.userStore.data?.id;
+    if (currentUserId) {
+      const idx = ids.indexOf(currentUserId);
+      if (idx > 0) {
+        const [self] = ids.splice(idx, 1);
+        ids.unshift(self);
+      }
+    }
+    return ids;

221-254: Minor: avoid O(n) includes for per‑member checks.

If called frequently, prefer a Set for filtered IDs to get O(1) lookups.

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

43-57: computedFn args hamper memoization; consider keying by projectId only.

Passing arrays by value limits computedFn caching. Fetch members/memberDetailsMap inside the computed to leverage MobX tracking and better memoization.

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

10-15: Avoid local type duplication.

Prefer importing IWorkspaceMembership from the workspace member store to prevent drift.

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

103-106: Default to a deterministic name sort when order_by is unset.

Returning unsorted arrays degrades UX and diverges from prior behavior. Use the existing parseOrderKey default ("display_name" asc).

Apply these diffs:

@@
 export const sortMembers = <T>(
@@
-): T[] => {
-  if (!orderBy) return members;
-
-  const { field, direction } = parseOrderKey(orderBy);
+): T[] => {
+  const { field, direction } = parseOrderKey(orderBy);
@@
-  // If no order_by filter, return filtered members
-  if (!filters?.order_by) return filteredMembers;
@@
-  // If no order_by filter, return filtered members
-  if (!filters?.order_by) return filteredMembers;

Also applies to: 150-151, 172-174

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0ed49a6 and 8055f73.

📒 Files selected for processing (26)
  • 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 (3 hunks)
  • apps/web/ce/store/member/project-member.store.ts (1 hunks)
  • apps/web/core/components/dropdowns/member/member-options.tsx (5 hunks)
  • apps/web/core/components/pages/version/editor.tsx (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/member-columns.tsx (5 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 (6 hunks)
  • packages/constants/src/index.ts (1 hunks)
  • packages/constants/src/members.ts (1 hunks)
  • packages/i18n/src/locales/en/translations.json (1 hunks)
  • packages/propel/src/icons/index.ts (1 hunks)
  • packages/propel/src/icons/suspended-user.tsx (1 hunks)
  • packages/propel/src/pill/index.ts (1 hunks)
  • packages/propel/src/pill/pill.tsx (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (13)
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/components/workspace/settings/member-columns.tsx (1)
packages/propel/src/icons/suspended-user.tsx (1)
  • SuspendedUserIcon (5-40)
apps/web/core/components/dropdowns/member/member-options.tsx (2)
apps/space/core/hooks/store/use-member.ts (1)
  • useMember (7-11)
packages/propel/src/icons/suspended-user.tsx (1)
  • SuspendedUserIcon (5-40)
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)
apps/web/core/store/member/utils.ts (4)
packages/constants/src/members.ts (1)
  • TMemberOrderByOptions (3-13)
packages/types/src/users.ts (1)
  • IUserLite (20-29)
apps/space/core/store/members.store.ts (1)
  • members (36-38)
packages/types/src/project/projects.ts (1)
  • TProjectMembership (93-107)
apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/members/page.tsx (2)
apps/web/core/components/project/dropdowns/filters/member-list.tsx (1)
  • MemberListFiltersDropdown (84-107)
packages/constants/src/event-tracker/core.ts (1)
  • MEMBER_TRACKER_ELEMENTS (249-258)
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/store/member/project/project-member-filters.store.ts (3)
apps/web/core/store/member/utils.ts (2)
  • IMemberFilters (5-8)
  • sortProjectMembers (140-161)
packages/types/src/project/projects.ts (1)
  • TProjectMembership (93-107)
packages/types/src/users.ts (1)
  • IUserLite (20-29)
apps/web/ce/components/workspace/settings/useMemberColumns.tsx (4)
apps/space/core/hooks/store/use-member.ts (1)
  • useMember (7-11)
apps/web/core/components/workspace/settings/member-columns.tsx (2)
  • RowData (21-25)
  • AccountTypeColumn (111-188)
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/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 (84-107)
packages/constants/src/event-tracker/core.ts (1)
  • MEMBER_TRACKER_ELEMENTS (249-258)
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 (163-183)
packages/types/src/users.ts (1)
  • IUserLite (20-29)
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-70)
apps/web/core/store/member/utils.ts (1)
  • sortProjectMembers (140-161)
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). (1)
  • GitHub Check: Build and lint web apps
🔇 Additional comments (25)
apps/web/core/components/pages/version/editor.tsx (1)

7-7: LGTM on import reorder

No functional change. Ensure linters/prettier rules are happy with the new position.

packages/propel/src/icons/index.ts (1)

59-59: LGTM: new icon is exposed via the barrel.

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

18-18: LGTM: re-exporting members constants.

Enables typed member property use across the app.

packages/propel/src/pill/index.ts (1)

1-1: LGTM: expose EPillVariant and EPillSize.

Matches downstream usage patterns.

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

32-32: LGTM: import path update aligns with store reorg.


229-232: Assign workspaceMemberMap to workSpaceMemberRolesMap (not memberMap)

Condition checks workspaceMemberMap but assigns memberMap — likely incorrect; change to assign workspaceMemberMap.

File: apps/web/core/store/issue/root.store.ts (lines ~229-232)

-      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;

Confirm workspace.workspaceMemberMap exists and the types align with workSpaceMemberRolesMap.

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

9-9: LGTM — import path & exports verified.
apps/web/core/store/member/workspace/workspace-member.store.ts exports IWorkspaceMemberStore and WorkspaceMemberStore; the import in apps/web/core/store/member/index.ts is correct.

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

28-33: OK to keep "suspended" — enum constant not present and store expects it

EUserWorkspaceRoles only defines ADMIN/MEMBER/GUEST (packages/types/src/workspace.ts); member filter logic checks for "suspended" (apps/web/core/store/member/utils.ts) and the dropdown uses that literal (apps/web/core/components/project/dropdowns/filters/member-list.tsx). Leave as-is.

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

16-16: Import path update aligns with new project member store.

Looks correct and consistent with the new project-scoped base store.

Please confirm the alias "@/store/member/project/base-project-member.store" is exported in tsconfig paths across all apps to avoid IDE/build drift.

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

8-8: Consistent re-path to project base store.

Matches the new project-scoped member store structure. No other changes required here.

apps/web/core/components/workspace/settings/members-list.tsx (1)

31-35: Good: filtered member IDs wiring.

Using getFilteredWorkspaceMemberIds is the right entry-point for respecting current filters.

packages/propel/src/pill/pill.tsx (3)

17-17: XS size addition looks good.

Enum and size map updates are consistent; default remains MD.


27-27: Type union correctly updated.

TPillSize includes XS; no breaking changes.


46-46: Padding/typography for XS is consistent.

Matches the design scale relative to other sizes.

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

31-35: Filters integration is correctly wired.

Using getFilters(projectId) and updateFilters(projectId, …) exposes sorting to headers without leaking store concerns into cells.

Confirm getFilters(projectId) returns an observable object to ensure MobX reactivity for header state.

Also applies to: 46-51


58-65: Header-based sorting hook-up looks solid.

MemberHeaderColumn usage for full_name, display_name, email, role, and joining_date is consistent and respects displayFilters.

Also applies to: 79-84, 88-98, 102-108, 121-129

apps/web/core/components/workspace/settings/member-columns.tsx (1)

24-24: New RowData.is_active dependency.

Ensure callers populate is_active for each row; otherwise suspended visuals won’t render.

apps/web/core/components/dropdowns/member/member-options.tsx (1)

153-161: Disable + cursor state are correct.

Combobox.Option disabled pairs with cursor-not-allowed; good for UX and a11y.

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

92-101: Role filter toggle logic looks good.

Toggles roles and clears when empty; aligns with store API.

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

54-68: Filter wiring looks correct.

Project-level role toggle persists via filters.getFilters/updateFilters and powers the dropdown state.

If roles are enum values, confirm MemberListFilters emits the same value shape expected by store filters (string vs enum).

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

20-23: Imports look correct and cohesive.

Utility and filters store imports are appropriate for the new filtering flow.


36-38: Good addition: per‑project filters on the store.

Public API surface looks right and aligns with downstream usage.


75-77: Good: filters store field on class.

Keeps the API symmetrical with the interface.


104-104: Initialization of filters store looks good.

No concerns.

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

49-62: LGTM: workspace filtered IDs derivation.

Wires into shared utils cleanly; behavior will improve with utils fixes below.

@vamsikrishnamathala vamsikrishnamathala marked this pull request as draft September 23, 2025 14:09
@vamsikrishnamathala vamsikrishnamathala marked this pull request as ready for review September 23, 2025 14:49
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: 2

🧹 Nitpick comments (3)
apps/web/core/components/dropdowns/member/member-options.tsx (3)

52-54: Avoid potential runtime crash on missing workspace.

Nested destructuring will throw if workspace is ever undefined. Prefer a safe accessor + fallback.

-  const {
-    workspace: { isUserSuspended },
-  } = useMember();
+  const memberStore = useMember();
+  const isUserSuspended = (userId: string) =>
+    memberStore.workspace?.isUserSuspended?.(userId) ?? false;

93-101: A11y: mark decorative icon hidden and avoid repeated store calls.

  • Add aria-hidden to the SuspendedUserIcon.
  • Minor: compute suspended once per user to avoid repeated reads.
-            <div className="w-4">
-              {isUserSuspended(userId) ? (
-                <SuspendedUserIcon className="h-3.5 w-3.5 text-custom-text-400" />
+            <div className="w-4">
+              {isUserSuspended(userId) ? (
+                <SuspendedUserIcon className="h-3.5 w-3.5 text-custom-text-400" aria-hidden="true" />
               ) : (
                 <Avatar name={userDetails?.display_name} src={getFileURL(userDetails?.avatar_url ?? "")} />
               )}
             </div>
-            <span className={cn("flex-grow truncate", isUserSuspended(userId) ? "text-custom-text-400" : "")}>
+            <span className={cn("flex-grow truncate", isUserSuspended(userId) ? "text-custom-text-400" : "")}>
               {currentUser?.id === userId ? t("you") : userDetails?.display_name}
             </span>

Optional (outside this range): precompute

const suspended = isUserSuspended(userId);

and reuse suspended instead of calling the function twice.


148-156: Ensure suspended users are placed at the bottom (per PR objective).

This component doesn’t reorder options; suspended users may appear intermixed. If upstream doesn’t already sort, add a stable sort here.

Outside this range, adjust filteredOptions:

// after computing `filteredOptions`:
const sortedOptions =
  filteredOptions?.slice().sort((a, b) => {
    const aSusp = isUserSuspended(a.value) ? 1 : 0;
    const bSusp = isUserSuspended(b.value) ? 1 : 0;
    return aSusp - bSusp; // non-suspended first
  });

Then iterate sortedOptions instead of filteredOptions. If memberIds are already pre-sorted upstream, ignore this.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 36498af and 6bbc67d.

📒 Files selected for processing (2)
  • apps/web/core/components/dropdowns/member/member-options.tsx (4 hunks)
  • apps/web/core/store/member/workspace/workspace-member.store.ts (8 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
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/core/components/dropdowns/member/member-options.tsx (2)
apps/space/core/hooks/store/use-member.ts (1)
  • useMember (7-11)
packages/propel/src/icons/suspended-user.tsx (1)
  • SuspendedUserIcon (5-40)
⏰ 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: Build and lint web apps
  • GitHub Check: Analyze (javascript)
🔇 Additional comments (7)
apps/web/core/store/member/workspace/workspace-member.store.ts (5)

16-16: Remove .ts extension in type import.

TypeScript disallows importing with .ts extensions by default unless allowImportingTsExtensions is enabled. This flag is only allowed when --noEmit or --emitDeclarationOnly is enabled, since these import paths would not be resolvable at runtime.

Apply this diff to fix the import:

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

31-31: LGTM! Well-integrated filters store.

The filtersStore property is properly declared in the interface, initialized in the constructor, and correctly typed. This provides a clean separation of filtering concerns.

Also applies to: 67-67, 94-94


38-38: LGTM! Filtered member IDs integration is well-implemented.

The new getFilteredWorkspaceMemberIds method properly:

  • Filters out bots and delegates to the filters store
  • Uses the established pattern with computedFn for performance
  • Provides a clean interface for filtered member access

Also applies to: 140-153


162-164: LGTM! Search integration updated correctly.

The search functionality now properly uses the filtered member IDs as the base set, maintaining the expected filtering behavior in search results.


57-57: LGTM! Suspended user detection is correctly implemented.

The isUserSuspended method properly:

  • Uses computedFn for performance optimization
  • Checks workspace member's is_active status correctly
  • Returns false when workspace context is unavailable

Also applies to: 91-91, 355-360

apps/web/core/components/dropdowns/member/member-options.tsx (2)

12-13: LGTM on new visual imports for suspension state.

Bringing in SuspendedUserIcon and Pill variants is appropriate for the UX you’re adding.


18-18: Confirmed: useMember hook exposes workspace.isUserSuspended (web)
apps/web/core/hooks/store/use-member.ts returns IMemberRootStore; workspace.isUserSuspended is implemented in apps/web/core/store/member/workspace/workspace-member.store.ts — no runtime mismatch.

@vamsikrishnamathala vamsikrishnamathala marked this pull request as draft September 24, 2025 09:40
@vamsikrishnamathala vamsikrishnamathala marked this pull request as ready for review September 24, 2025 09:44
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 (4)
apps/web/core/store/member/utils.ts (4)

41-48: Fix joining_date sentinel (current comment is incorrect; Date(0) sorts first on asc).

Return an invalid date and handle nulls-last in the comparator instead, so missing/invalid dates deterministically fall to the bottom.

-    case "joining_date": {
-      if (!memberDetails.joining_date) {
-        // Return a very old date for missing dates to sort them last
-        return new Date(0);
-      }
-      const date = new Date(memberDetails.joining_date);
-      // Return a very old date for invalid dates to sort them last
-      return isNaN(date.getTime()) ? new Date(0) : date;
-    }
+    case "joining_date": {
+      if (!memberDetails.joining_date) {
+        // Return an invalid date; comparator will place invalid/missing at the bottom (nulls-last)
+        return new Date(NaN);
+      }
+      const date = new Date(memberDetails.joining_date);
+      // If invalid, return invalid; comparator will handle it
+      return Number.isNaN(date.getTime()) ? new Date(NaN) : date;
+    }

112-113: Push entries with missing memberDetails to the bottom (deterministic order).

Returning 0 for missing details can cause unstable ordering. Push them to the end.

-    if (!aMemberDetails || !bMemberDetails) return 0;
+    if (!aMemberDetails && !bMemberDetails) return 0;
+    if (!aMemberDetails) return 1;
+    if (!bMemberDetails) return -1;

56-67: Optional: Make project-role filtering case-insensitive.

If UI tokens differ in case from stored roles, includes() may fail. Normalize both sides to lower-case.

 export const filterProjectMembersByRole = (
   members: TProjectMembership[],
   roleFilters: string[]
 ): TProjectMembership[] => {
   if (roleFilters.length === 0) return members;
 
-  return members.filter((member) => {
-    const memberRole = String(member.role ?? member.original_role ?? "");
-    return roleFilters.includes(memberRole);
-  });
+  const roleSet = new Set(roleFilters.map((r) => r.toLowerCase()));
+  return members.filter((member) => {
+    const memberRole = String(member.role ?? member.original_role ?? "").toLowerCase();
+    return roleSet.has(memberRole);
+  });
 };

28-54: Optional: Role sort may be inconsistent if EUserPermissions is numeric.

If role is a numeric enum, string sorting (“10” < “2”) is undesirable. Consider sorting by a user-facing label or an explicit precedence map.

Can you confirm whether role values are strings (labels) or numeric enums in workspace/project contexts?

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6bbc67d and e94718c.

📒 Files selected for processing (1)
  • apps/web/core/store/member/utils.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
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)
⏰ 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: Build and lint web apps
🔇 Additional comments (3)
apps/web/core/store/member/utils.ts (3)

69-92: Role filter union semantics for “suspended” look correct.

Suspended users are included only when “suspended” is selected, and active users are filtered by the remaining roles (OR semantics). LGTM.


122-139: Make date comparator nulls-last and robust to invalid/missing dates.

Avoid treating invalid dates as epoch (which floats them to the top). Push invalid/missing to the bottom on asc (and top on desc via final direction flip).

-    if (field === "joining_date") {
-      // For dates, we need to handle Date objects and ensure they're valid
-      const aDate = aValue instanceof Date ? aValue : new Date(aValue);
-      const bDate = bValue instanceof Date ? bValue : new Date(bValue);
-
-      // Handle invalid dates by treating them as very old dates
-      const aTime = isNaN(aDate.getTime()) ? 0 : aDate.getTime();
-      const bTime = isNaN(bDate.getTime()) ? 0 : bDate.getTime();
-
-      comparison = aTime - bTime;
-    } else {
-      // For strings, use localeCompare for proper alphabetical sorting
-      const aStr = String(aValue);
-      const bStr = String(bValue);
-      comparison = aStr.localeCompare(bStr);
-    }
+    if (field === "joining_date") {
+      // For dates, handle invalid/missing as nulls-last
+      const aDate = aValue instanceof Date ? aValue : new Date(aValue as any);
+      const bDate = bValue instanceof Date ? bValue : new Date(bValue as any);
+      const aTime = aDate.getTime();
+      const bTime = bDate.getTime();
+      const aInvalid = Number.isNaN(aTime);
+      const bInvalid = Number.isNaN(bTime);
+      if (aInvalid && bInvalid) comparison = 0;
+      else if (aInvalid) comparison = 1; // push invalid/missing to bottom on asc
+      else if (bInvalid) comparison = -1;
+      else comparison = aTime - bTime;
+    } else {
+      // For strings, use localeCompare for proper alphabetical sorting
+      const aStr = String(aValue ?? "");
+      const bStr = String(bValue ?? "");
+      comparison = aStr.localeCompare(bStr);
+    }

167-187: Guarantee suspended users are always listed last (partition before sort).

Currently, suspended users can appear anywhere when order_by is applied. Partition into active/suspended, sort each partition, then concatenate to keep suspended at the bottom (per PR objective).

 export const sortWorkspaceMembers = <T extends { role: string | EUserPermissions; is_active?: boolean }>(
   members: T[],
   memberDetailsMap: Record<string, IUserLite>,
   getMemberKey: (member: T) => string,
   filters?: IMemberFilters
 ): T[] => {
-  const filteredMembers =
-    filters?.roles && filters.roles.length > 0 ? filterWorkspaceMembersByRole(members, filters.roles) : members;
-
-  // If no order_by filter, return filtered members
-  if (!filters?.order_by) return filteredMembers;
-
-  // Apply sorting
-  return sortMembers(
-    filteredMembers,
-    memberDetailsMap,
-    getMemberKey,
-    (member) => String(member.role ?? ""),
-    filters.order_by
-  );
+  const filteredMembers =
+    filters?.roles && filters.roles.length > 0 ? filterWorkspaceMembersByRole(members, filters.roles) : members;
+
+  // Partition to keep suspended at the bottom
+  const activeMembers = filteredMembers.filter((m) => m?.is_active !== false);
+  const suspendedMembers = filteredMembers.filter((m) => m?.is_active === false);
+
+  const sortedActive = sortMembers(
+    activeMembers,
+    memberDetailsMap,
+    getMemberKey,
+    (member) => String(member.role ?? ""),
+    filters?.order_by
+  );
+
+  const sortedSuspended = sortMembers(
+    suspendedMembers,
+    memberDetailsMap,
+    getMemberKey,
+    (member) => String(member.role ?? ""),
+    filters?.order_by
+  );
+
+  return [...sortedActive, ...sortedSuspended];
 }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🌟enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant