Skip to content

feat: Add member filtering support to workspace and project member list endpoints#53

Merged
Prashant-Surya merged 3 commits into
mainfrom
chore-memeber-fields-filters
Jun 24, 2026
Merged

feat: Add member filtering support to workspace and project member list endpoints#53
Prashant-Surya merged 3 commits into
mainfrom
chore-memeber-fields-filters

Conversation

@gurusainath

@gurusainath gurusainath commented Jun 24, 2026

Copy link
Copy Markdown
Member

Description

Added support for filtering workspace and project members through SDK member listing APIs.

This change introduces typed query parameters that mirror the filtering capabilities available in the external REST APIs.

Changes

  • Added MemberQueryParams model for member filtering.
  • Added filtering support to:
    • client.workspaces.get_members()
    • client.projects.get_members()
  • Supported filters:
    • first_name
    • last_name
    • email
    • display_name
    • role_slug
    • is_active
    • is_bot
  • Added support for both typed query parameter models and raw dictionaries.
  • Exported MemberQueryParams from plane.models.

Backward Compatibility

  • Fully backward compatible.
  • Existing member list calls continue to work without any changes.
  • Empty and unset filters are automatically excluded from requests.
  • Unknown fields are safely ignored.

Type of Change

  • Feature (non-breaking change which adds functionality)

Test Scenarios

  • Verified workspace member filtering using typed query parameters.
  • Verified project member filtering using typed query parameters.
  • Tested filtering using raw dictionary inputs for backward compatibility.
  • Validated query parameter serialization and omission of unset values.
  • Confirmed unknown parameters are ignored safely.
  • Verified existing member list APIs continue to function unchanged.

Summary by CodeRabbit

  • New Features
    • Workspace and project member listing now supports typed filtering via MemberQueryParams, including first_name/last_name/email/display_name, role_slug, and is_active/is_bot.
    • “Lite” member listing supports paginated querying via MemberListQueryParams, returning a paginated envelope with results and total_count.
    • Returned member objects now include optional is_active and is_bot fields (for both workspaces and projects).
  • Documentation
    • Updated README examples to demonstrate member filtering and lite pagination.
  • Tests
    • Added/expanded smoke tests for typed query params and backward-compatible raw dict inputs.

@gurusainath gurusainath self-assigned this Jun 24, 2026
@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@gurusainath, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 42 minutes and 45 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0249b51c-432b-444c-ad2e-3b26ec0fe14e

📥 Commits

Reviewing files that changed from the base of the PR and between ae4b7e6 and 5085f43.

📒 Files selected for processing (3)
  • README.md
  • tests/unit/test_projects.py
  • tests/unit/test_workspaces.py
📝 Walkthrough

Walkthrough

The PR adds typed member query parameters, extends workspace and project member schemas, updates member-list endpoints to use them, and refreshes tests, README examples, and package metadata.

Changes

Member filtering and pagination

Layer / File(s) Summary
Query params and member schemas
plane/models/query_params.py, plane/models/workspaces.py, plane/models/projects.py, plane/models/__init__.py
Adds MemberQueryParams and MemberListQueryParams, adds is_active and is_bot to workspace/project member models, introduces paginated member response models, and re-exports MemberQueryParams.
Workspace and project member endpoints
plane/api/workspaces.py, plane/api/projects.py
Updates member listing methods to accept typed member query params, serialize them into query strings, and use the updated member endpoint paths where changed.
Tests and usage examples
tests/unit/test_workspaces.py, tests/unit/test_projects.py, README.md
Adds smoke tests for typed and raw member filtering, plus README examples for filtered lookups and lite pagination.
Package version bump
pyproject.toml
Bumps the package version from 0.2.17 to 0.2.18.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • sriramveeraghanta
  • dheeru0198
  • Prashant-Surya

Poem

🐇 I hop through filters, swift and keen,
With query params both typed and clean.
Members page and members lite,
Now land in docs and tests just right.
A carrot-toast to schemas new—
The bunny SDK grew a fresher view!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding member filtering support to workspace and project member list endpoints.
Docstring Coverage ✅ Passed Docstring coverage is 95.45% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore-memeber-fields-filters

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

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (1)
plane/api/workspaces.py (1)

15-29: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Workspaces.get_members() no longer accepts raw dict params.

Line 28 assumes any truthy params has model_dump(), so an existing call like get_members(..., params={"is_active": True}) now raises AttributeError. That breaks the PR's stated backward-compatible dict support and makes workspaces inconsistent with Projects.get_members().

Suggested fix
+from collections.abc import Mapping
 from typing import Any
@@
-    def get_members(
-        self, workspace_slug: str, params: MemberQueryParams | None = None
-    ) -> list[WorkspaceMember]:
+    def get_members(
+        self,
+        workspace_slug: str,
+        params: MemberQueryParams | Mapping[str, Any] | None = None,
+    ) -> list[WorkspaceMember]:
@@
-        response = self._get(
-            f"{workspace_slug}/members",
-            params=params.model_dump(exclude_none=True) if params else None,
-        )
+        if isinstance(params, MemberQueryParams):
+            params = params.model_dump(exclude_none=True)
+        elif params is not None:
+            params = {key: value for key, value in params.items() if value not in (None, "")}
+
+        response = self._get(f"{workspace_slug}/members/", params=params)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plane/api/workspaces.py` around lines 15 - 29, Workspaces.get_members()
currently assumes params is always a MemberQueryParams instance and calls
model_dump(), which breaks existing raw dict callers with AttributeError. Update
the Workspaces.get_members method to accept both MemberQueryParams and dict
inputs, matching the behavior of Projects.get_members(). Normalize params before
passing it to self._get so dicts are forwarded directly and model_dump() is only
used when params is a model instance.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@plane/api/projects.py`:
- Around line 102-107: The members query in the project API still forwards empty
filter values when params is a raw mapping, so the documented “empty filters are
omitted” behavior is only applied in the MemberQueryParams path. Update the
normalization in the projects members request flow so both MemberQueryParams and
mapping inputs are cleaned before calling _get, using the existing params
handling in the projects API method to drop empty string/empty-like values as
well as None. Keep the fix localized around the member listing method that
builds the /members request.

In `@plane/api/workspaces.py`:
- Around line 26-28: The workspace members request is still using the
non-trailing-slash path, so update the endpoint in the members fetch logic to
use the trailing slash form. In the method that builds the `_get` call for
workspace members, change the `f"{workspace_slug}/members"` target to the
trailing-slash variant so it matches the API path convention used across the
client.

In `@plane/models/query_params.py`:
- Around line 93-123: The blank-string filters in MemberQueryParams are still
being serialized because only None is excluded, so empty values like
display_name="" leak into member-list requests. Update the MemberQueryParams
handling so empty strings are treated as unset and omitted during serialization,
likely by normalizing blank text fields to None in the model or by adding a
field-level serializer/validator on the text filter fields (first_name,
last_name, email, display_name). Ensure the behavior matches the
BaseQueryParams/member-list contract that empty filters are not sent.

---

Outside diff comments:
In `@plane/api/workspaces.py`:
- Around line 15-29: Workspaces.get_members() currently assumes params is always
a MemberQueryParams instance and calls model_dump(), which breaks existing raw
dict callers with AttributeError. Update the Workspaces.get_members method to
accept both MemberQueryParams and dict inputs, matching the behavior of
Projects.get_members(). Normalize params before passing it to self._get so dicts
are forwarded directly and model_dump() is only used when params is a model
instance.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b5d71c47-7b16-48f9-b86c-8c26570d4744

📥 Commits

Reviewing files that changed from the base of the PR and between ce9c0b3 and af74e32.

📒 Files selected for processing (8)
  • plane/api/projects.py
  • plane/api/workspaces.py
  • plane/models/__init__.py
  • plane/models/projects.py
  • plane/models/query_params.py
  • plane/models/workspaces.py
  • tests/unit/test_projects.py
  • tests/unit/test_workspaces.py

Comment thread plane/api/projects.py Outdated
Comment on lines 102 to 107
params: Optional query parameters. Accepts a ``MemberQueryParams``
instance for typed filtering/ordering, or a raw mapping.
"""
if isinstance(params, MemberQueryParams):
params = params.model_dump(exclude_none=True)
response = self._get(f"{workspace_slug}/projects/{project_id}/members", params=params)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Backcompat mappings still forward empty filters.

Only the MemberQueryParams branch is normalized here, and even that only drops None. A raw mapping like {"display_name": ""} is forwarded straight to _get, so the backcompat path does not match the documented "empty filters are omitted" behavior.

Suggested fix
         if isinstance(params, MemberQueryParams):
             params = params.model_dump(exclude_none=True)
+        if params is not None:
+            params = {key: value for key, value in params.items() if value not in (None, "")}
         response = self._get(f"{workspace_slug}/projects/{project_id}/members", params=params)
📝 Committable suggestion

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

Suggested change
params: Optional query parameters. Accepts a ``MemberQueryParams``
instance for typed filtering/ordering, or a raw mapping.
"""
if isinstance(params, MemberQueryParams):
params = params.model_dump(exclude_none=True)
response = self._get(f"{workspace_slug}/projects/{project_id}/members", params=params)
params: Optional query parameters. Accepts a ``MemberQueryParams``
instance for typed filtering/ordering, or a raw mapping.
"""
if isinstance(params, MemberQueryParams):
params = params.model_dump(exclude_none=True)
if params is not None:
params = {key: value for key, value in params.items() if value not in (None, "")}
response = self._get(f"{workspace_slug}/projects/{project_id}/members", params=params)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plane/api/projects.py` around lines 102 - 107, The members query in the
project API still forwards empty filter values when params is a raw mapping, so
the documented “empty filters are omitted” behavior is only applied in the
MemberQueryParams path. Update the normalization in the projects members request
flow so both MemberQueryParams and mapping inputs are cleaned before calling
_get, using the existing params handling in the projects API method to drop
empty string/empty-like values as well as None. Keep the fix localized around
the member listing method that builds the /members request.

Comment thread plane/api/workspaces.py Outdated
Comment on lines +26 to +28
response = self._get(
f"{workspace_slug}/members",
params=params.model_dump(exclude_none=True) if params else None,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use the trailing-slash member endpoint.

This changed request still targets f"{workspace_slug}/members" instead of the required trailing-slash form. As per path instructions, all API endpoints should end with a trailing /.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plane/api/workspaces.py` around lines 26 - 28, The workspace members request
is still using the non-trailing-slash path, so update the endpoint in the
members fetch logic to use the trailing slash form. In the method that builds
the `_get` call for workspace members, change the `f"{workspace_slug}/members"`
target to the trailing-slash variant so it matches the API path convention used
across the client.

Source: Path instructions

Comment on lines +93 to +123
class MemberQueryParams(BaseQueryParams):
"""Query parameters for workspace/project member list endpoints.

Inherits the documented query parameters from BaseQueryParams (expand,
fields, external_id, external_source, order_by) and adds member-specific
filters. Text filters match case-insensitively on a substring; ``role_slug``
matches exactly. Boolean filters narrow by membership/account flags.
"""

model_config = ConfigDict(extra="ignore", populate_by_name=True)

# text filters
first_name: str | None = Field(
None, description="Filter by member first name (case-insensitive contains)"
)
last_name: str | None = Field(
None, description="Filter by member last name (case-insensitive contains)"
)
email: str | None = Field(
None, description="Filter by member email (case-insensitive contains)"
)
display_name: str | None = Field(
None, description="Filter by member display name (case-insensitive contains)"
)
role_slug: str | None = Field(None, description="Filter by role slug (exact match)")

# boolean filters
is_active: bool | None = Field(None, description="Filter by active membership status")
is_bot: bool | None = Field(None, description="Filter by bot accounts")


Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Blank string filters will still be sent.

model_dump(exclude_none=True) only drops None, so MemberQueryParams(display_name="") still serializes as display_name=. That contradicts the PR contract that empty filters are omitted and will leak blank filters into both typed member-list endpoints.

Suggested fix
+from pydantic import ConfigDict, Field, field_validator
+
 class MemberQueryParams(BaseQueryParams):
@@
     role_slug: str | None = Field(None, description="Filter by role slug (exact match)")
@@
     is_active: bool | None = Field(None, description="Filter by active membership status")
     is_bot: bool | None = Field(None, description="Filter by bot accounts")
+
+    `@field_validator`("first_name", "last_name", "email", "display_name", "role_slug", mode="before")
+    `@classmethod`
+    def _empty_strings_to_none(cls, value: str | None) -> str | None:
+        if isinstance(value, str) and not value.strip():
+            return None
+        return value
📝 Committable suggestion

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

Suggested change
class MemberQueryParams(BaseQueryParams):
"""Query parameters for workspace/project member list endpoints.
Inherits the documented query parameters from BaseQueryParams (expand,
fields, external_id, external_source, order_by) and adds member-specific
filters. Text filters match case-insensitively on a substring; ``role_slug``
matches exactly. Boolean filters narrow by membership/account flags.
"""
model_config = ConfigDict(extra="ignore", populate_by_name=True)
# text filters
first_name: str | None = Field(
None, description="Filter by member first name (case-insensitive contains)"
)
last_name: str | None = Field(
None, description="Filter by member last name (case-insensitive contains)"
)
email: str | None = Field(
None, description="Filter by member email (case-insensitive contains)"
)
display_name: str | None = Field(
None, description="Filter by member display name (case-insensitive contains)"
)
role_slug: str | None = Field(None, description="Filter by role slug (exact match)")
# boolean filters
is_active: bool | None = Field(None, description="Filter by active membership status")
is_bot: bool | None = Field(None, description="Filter by bot accounts")
from pydantic import ConfigDict, Field, field_validator
class MemberQueryParams(BaseQueryParams):
"""Query parameters for workspace/project member list endpoints.
Inherits the documented query parameters from BaseQueryParams (expand,
fields, external_id, external_source, order_by) and adds member-specific
filters. Text filters match case-insensitively on a substring; ``role_slug``
matches exactly. Boolean filters narrow by membership/account flags.
"""
model_config = ConfigDict(extra="ignore", populate_by_name=True)
# text filters
first_name: str | None = Field(
None, description="Filter by member first name (case-insensitive contains)"
)
last_name: str | None = Field(
None, description="Filter by member last name (case-insensitive contains)"
)
email: str | None = Field(
None, description="Filter by member email (case-insensitive contains)"
)
display_name: str | None = Field(
None, description="Filter by member display name (case-insensitive contains)"
)
role_slug: str | None = Field(None, description="Filter by role slug (exact match)")
# boolean filters
is_active: bool | None = Field(None, description="Filter by active membership status")
is_bot: bool | None = Field(None, description="Filter by bot accounts")
`@field_validator`("first_name", "last_name", "email", "display_name", "role_slug", mode="before")
`@classmethod`
def _empty_strings_to_none(cls, value: str | None) -> str | None:
if isinstance(value, str) and not value.strip():
return None
return value
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plane/models/query_params.py` around lines 93 - 123, The blank-string filters
in MemberQueryParams are still being serialized because only None is excluded,
so empty values like display_name="" leak into member-list requests. Update the
MemberQueryParams handling so empty strings are treated as unset and omitted
during serialization, likely by normalizing blank text fields to None in the
model or by adding a field-level serializer/validator on the text filter fields
(first_name, last_name, email, display_name). Ensure the behavior matches the
BaseQueryParams/member-list contract that empty filters are not sent.

…s in the existing and handled the pagination for new lite endpoints

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (3)
plane/api/workspaces.py (1)

31-33: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use the trailing-slash member endpoints here.

These changed endpoints still omit the terminal /, which violates the API path convention and can fail against routers that do not normalize slashless paths. As per path instructions, All API endpoints should end with a trailing / and follow URL convention: {base_path}/api/v1{resource_base_path}/{endpoint}/.

Also applies to: 51-53

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plane/api/workspaces.py` around lines 31 - 33, The member-list endpoints in
the workspace API calls are missing the required trailing slash, which can break
routing on slash-sensitive servers. Update the request path built in the
workspace members helper to use the trailing-slash form, and apply the same fix
to the other related members call referenced in this review so the endpoint path
consistently follows the API convention. Look for the self._get usages in the
workspace members methods and ensure the final URL segment ends with /.

Source: Path instructions

plane/models/query_params.py (1)

123-132: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Blank text filters still leak into the query string.

model_dump(exclude_none=True) only drops None, so values like display_name="" or " " still serialize and get sent by both member-list APIs. That breaks the PR contract that empty filters are omitted.

Suggested fix
     def to_query_params(self) -> dict[str, Any]:
         """Serialize to a query-param dict the member endpoints accept.
@@
-        raw = self.model_dump(exclude_none=True)
+        raw = {
+            k: v
+            for k, v in self.model_dump(exclude_none=True).items()
+            if not (isinstance(v, str) and not v.strip())
+        }
         return {k: (str(v).lower() if isinstance(v, bool) else v) for k, v in raw.items()}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plane/models/query_params.py` around lines 123 - 132, The to_query_params
method in QueryParams still serializes empty string filters because
model_dump(exclude_none=True) only removes None values. Update the serialization
logic to also drop blank text values like "" and whitespace-only strings before
building the query-param dict, while preserving the existing boolean lowercasing
behavior for accepted fields.
plane/api/projects.py (1)

112-119: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Raw mapping filters still forward empty values.

The backcompat branch lowercases bools, but it still passes None and blank strings straight through to _get(...). That means {"display_name": ""} does not honor the documented “empty filters are omitted” behavior.

Suggested fix
         if isinstance(params, MemberQueryParams):
             params = params.to_query_params()
         elif params is not None:
             # Lowercase bools so the typed filter backend accepts them
             # (requests would otherwise encode True as "True" -> HTTP 400).
-            params = {k: (str(v).lower() if isinstance(v, bool) else v) for k, v in params.items()}
+            params = {
+                k: (str(v).lower() if isinstance(v, bool) else v)
+                for k, v in params.items()
+                if v is not None and not (isinstance(v, str) and not v.strip())
+            }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plane/api/projects.py` around lines 112 - 119, Empty mapping filter values
are still being sent through in the project members query path. Update the
params handling in the project-members request flow around the MemberQueryParams
/ raw mapping branch so blank strings and None are filtered out before calling
_get(...), while still lowercasing bools for the typed filter backend. Keep the
behavior consistent with the existing query param conversion helper used by the
project member listing logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@plane/api/projects.py`:
- Around line 118-119: The project member endpoints in the `Projects` API are
missing the required trailing slash, which can cause route mismatches on
stricter servers. Update the `_get` calls for the `project-members` and related
member endpoint in `plane/api/projects.py` to use the conventionally formatted
path ending in `/`, matching the docstring and the API URL pattern used
throughout `plane/api/**/*.py`.

In `@plane/api/workspaces.py`:
- Around line 18-35: Workspaces.get_members() should preserve the raw-dictionary
backcompat path instead of only handling MemberQueryParams. Update the
get_members method in Workspaces so callers can still pass a plain mapping like
{"is_active": True} without hitting params.to_query_params(); keep the existing
MemberQueryParams behavior while adding a branch that forwards raw dict-like
params directly to _get. Ensure the response parsing into WorkspaceMember stays
unchanged.

---

Duplicate comments:
In `@plane/api/projects.py`:
- Around line 112-119: Empty mapping filter values are still being sent through
in the project members query path. Update the params handling in the
project-members request flow around the MemberQueryParams / raw mapping branch
so blank strings and None are filtered out before calling _get(...), while still
lowercasing bools for the typed filter backend. Keep the behavior consistent
with the existing query param conversion helper used by the project member
listing logic.

In `@plane/api/workspaces.py`:
- Around line 31-33: The member-list endpoints in the workspace API calls are
missing the required trailing slash, which can break routing on slash-sensitive
servers. Update the request path built in the workspace members helper to use
the trailing-slash form, and apply the same fix to the other related members
call referenced in this review so the endpoint path consistently follows the API
convention. Look for the self._get usages in the workspace members methods and
ensure the final URL segment ends with /.

In `@plane/models/query_params.py`:
- Around line 123-132: The to_query_params method in QueryParams still
serializes empty string filters because model_dump(exclude_none=True) only
removes None values. Update the serialization logic to also drop blank text
values like "" and whitespace-only strings before building the query-param dict,
while preserving the existing boolean lowercasing behavior for accepted fields.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 62b6d6e1-77f8-42a1-bf63-fbf6470cd916

📥 Commits

Reviewing files that changed from the base of the PR and between af74e32 and ae4b7e6.

📒 Files selected for processing (10)
  • README.md
  • plane/api/projects.py
  • plane/api/workspaces.py
  • plane/models/__init__.py
  • plane/models/projects.py
  • plane/models/query_params.py
  • plane/models/workspaces.py
  • pyproject.toml
  • tests/unit/test_projects.py
  • tests/unit/test_workspaces.py
✅ Files skipped from review due to trivial changes (1)
  • pyproject.toml
🚧 Files skipped from review as they are similar to previous changes (4)
  • plane/models/init.py
  • tests/unit/test_projects.py
  • tests/unit/test_workspaces.py
  • plane/models/workspaces.py

Comment thread plane/api/projects.py
Comment on lines +118 to +119
response = self._get(
f"{workspace_slug}/projects/{project_id}/project-members", params=params

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

These member endpoints should include the trailing slash.

The docstring already describes /project-members/, but both requests still hit slashless paths. That violates the API endpoint convention for plane/api/**/*.py and risks route mismatches on stricter servers. As per path instructions, All API endpoints should end with a trailing / and follow URL convention: {base_path}/api/v1{resource_base_path}/{endpoint}/.

Also applies to: 141-143

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plane/api/projects.py` around lines 118 - 119, The project member endpoints
in the `Projects` API are missing the required trailing slash, which can cause
route mismatches on stricter servers. Update the `_get` calls for the
`project-members` and related member endpoint in `plane/api/projects.py` to use
the conventionally formatted path ending in `/`, matching the docstring and the
API URL pattern used throughout `plane/api/**/*.py`.

Source: Path instructions

Comment thread plane/api/workspaces.py
Comment on lines 18 to 35
def get_members(
self, workspace_slug: str
self, workspace_slug: str, params: MemberQueryParams | None = None
) -> list[WorkspaceMember]:
"""Get all members of a workspace.
"""Get all members of a workspace (unpaginated).

Returns a list of WorkspaceMember objects that include role (int) and
role_slug (str) fields in addition to basic identity fields.

Args:
workspace_slug: The workspace slug identifier
params: Optional filter query parameters (first_name, last_name,
email, display_name, role_slug, is_active, is_bot)
"""
response = self._get(f"{workspace_slug}/members")
response = self._get(
f"{workspace_slug}/members",
params=params.to_query_params() if params else None,
)
return [WorkspaceMember.model_validate(item) for item in response or []]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Workspaces.get_members() lost the raw-mapping backcompat path.

The PR contract says workspace member listing still accepts raw dictionaries, but this signature only accepts MemberQueryParams and immediately calls to_query_params(). Existing callers that pass {"is_active": True} will now fail before the request is made.

Suggested fix
+from collections.abc import Mapping
+
     def get_members(
-        self, workspace_slug: str, params: MemberQueryParams | None = None
+        self,
+        workspace_slug: str,
+        params: MemberQueryParams | Mapping[str, Any] | None = None,
     ) -> list[WorkspaceMember]:
@@
-        response = self._get(
-            f"{workspace_slug}/members",
-            params=params.to_query_params() if params else None,
-        )
+        if isinstance(params, MemberQueryParams):
+            params = params.to_query_params()
+        elif params is not None:
+            params = {
+                k: (str(v).lower() if isinstance(v, bool) else v)
+                for k, v in params.items()
+            }
+        response = self._get(f"{workspace_slug}/members", params=params)
         return [WorkspaceMember.model_validate(item) for item in response or []]
📝 Committable suggestion

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

Suggested change
def get_members(
self, workspace_slug: str
self, workspace_slug: str, params: MemberQueryParams | None = None
) -> list[WorkspaceMember]:
"""Get all members of a workspace.
"""Get all members of a workspace (unpaginated).
Returns a list of WorkspaceMember objects that include role (int) and
role_slug (str) fields in addition to basic identity fields.
Args:
workspace_slug: The workspace slug identifier
params: Optional filter query parameters (first_name, last_name,
email, display_name, role_slug, is_active, is_bot)
"""
response = self._get(f"{workspace_slug}/members")
response = self._get(
f"{workspace_slug}/members",
params=params.to_query_params() if params else None,
)
return [WorkspaceMember.model_validate(item) for item in response or []]
from collections.abc import Mapping
def get_members(
self,
workspace_slug: str,
params: MemberQueryParams | Mapping[str, Any] | None = None,
) -> list[WorkspaceMember]:
"""Get all members of a workspace (unpaginated).
Returns a list of WorkspaceMember objects that include role (int) and
role_slug (str) fields in addition to basic identity fields.
Args:
workspace_slug: The workspace slug identifier
params: Optional filter query parameters (first_name, last_name,
email, display_name, role_slug, is_active, is_bot)
"""
if isinstance(params, MemberQueryParams):
params = params.to_query_params()
elif params is not None:
params = {
k: (str(v).lower() if isinstance(v, bool) else v)
for k, v in params.items()
}
response = self._get(
f"{workspace_slug}/members",
params=params,
)
return [WorkspaceMember.model_validate(item) for item in response or []]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plane/api/workspaces.py` around lines 18 - 35, Workspaces.get_members()
should preserve the raw-dictionary backcompat path instead of only handling
MemberQueryParams. Update the get_members method in Workspaces so callers can
still pass a plain mapping like {"is_active": True} without hitting
params.to_query_params(); keep the existing MemberQueryParams behavior while
adding a branch that forwards raw dict-like params directly to _get. Ensure the
response parsing into WorkspaceMember stays unchanged.

@Prashant-Surya Prashant-Surya merged commit 58ad199 into main Jun 24, 2026
4 checks passed
@Prashant-Surya Prashant-Surya deleted the chore-memeber-fields-filters branch June 24, 2026 16:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants