[WEB-4896]feat: filters to project and workspace members list#7786
[WEB-4896]feat: filters to project and workspace members list#7786
Conversation
|
Note Other AI code review bot(s) detectedCodeRabbit 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. WalkthroughAdds 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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Linked to Plane Work Item(s) This comment was auto-generated by Plane |
There was a problem hiding this comment.
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 memberMapLocation: 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
getFilteredWorkspaceMemberIdsso 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
undefinedand 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
useMemokeyed 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
memberIdbut the function takesinvitationId.- * @param memberId + * @param invitationIdapps/web/core/components/project/member-header-column.tsx (2)
65-99: Localize sorting labels.
ascendingOrderTitle/descendingOrderTitleare 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
getFilteredProjectMemberDetailsin 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 useget(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.getFilteredMemberIdsfor 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
📒 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
undefinedis correct and MobX-friendly.
143-147: Filters dropdown integration: LGTM.Wiring
appliedFiltersandhandleUpdateis correct.
161-161: Search likely ignores applied role filters in workspace view.
WorkspaceMembersListcurrently searches over unfilteredworkspaceMemberIds, 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 inworkspace-member.store.tsfor 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.
workspaceMemberIdsprioritizes current user then alpha;getFilteredWorkspaceMemberIdsdelegates tosortWorkspaceMemberswhich may not. Please confirm the utility preserves this default when no explicitorder_byis set.Also applies to: 136-154
91-93: Filters lifetime across workspaces.
filtersStoreis singleton for the store; consider resetting filters whenworkspaceSlugchanges 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.
There was a problem hiding this comment.
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
📒 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
undefinedwhen no roles are selected- Updates the store appropriately
105-105: LGTM! Proper derived state for filter dropdown.The
appliedRoleFiltersis 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
filtersStorefrom the workspace object returned byuseMember(). Ensure this property is available and properly typed in the hook's return value.
143-147: Verify MemberListFiltersDropdown accepts workspace memberType.Ensure the
MemberListFiltersDropdowncomponent properly handles thememberType="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
…ane into feat-project_member_filters
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (5)
apps/web/core/store/member/workspace/workspace-member.store.ts (5)
15-17: Avoid.tsextension in import path unless using NodeNext ESM.Using
../index.tscan break under common TS/Next setups that expect extensionless or.js-suffixed imports at runtime. Prefer../indexfor 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 markingfiltersStoreas readonly in the interface.Conveys intent and prevents accidental reassignment.
Apply this diff:
- filtersStore: IWorkspaceMemberFiltersStore; + readonly filtersStore: IWorkspaceMemberFiltersStore;
65-66: Make the class propertyfiltersStorereadonly 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 ingetWorkspaceMemberIds.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.
getFilteredWorkspaceMemberIdsalways 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
📒 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.
filtersStoreholds a singlefiltersobject shared across workspaces. Is that intentional? If filters should be per‑workspace, consider keying byworkspaceSlugor resetting on workspace change.
There was a problem hiding this comment.
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
📒 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 arggetFilters(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.
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
Screenshots and Media (if applicable)
Screen.Recording.2025-09-12.at.5.01.10.PM.mov
Test Scenarios
References
Summary by CodeRabbit
New Features
Enhancements
Localization