Skip to content

feat: add custom state group ordering (Fixes #8743)#8745

Open
Aesthetic002 wants to merge 2 commits intomakeplane:previewfrom
Aesthetic002:fix-state-group-order
Open

feat: add custom state group ordering (Fixes #8743)#8745
Aesthetic002 wants to merge 2 commits intomakeplane:previewfrom
Aesthetic002:fix-state-group-order

Conversation

@Aesthetic002
Copy link

@Aesthetic002 Aesthetic002 commented Mar 10, 2026

Description

This PR implements the ability to customize the display order of state groups within a project.

A new field state_group_order has been added to the Project model to persist the order of state groups. The frontend state store now reads this value and applies the custom ordering when grouping project states.

Drag-and-drop functionality has been added to the Project Settings → States page to allow users to reorder state groups. The updated order is persisted using the project update API and reflected across the application, including the Kanban board.

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)

N/A

Test Scenarios

  1. Started backend services using Docker and ran database migrations.
  2. Started the frontend development server using pnpm dev.
  3. Navigated to Project → Settings → States.
  4. Reordered state groups using drag-and-drop.
  5. Refreshed the page to verify that the order persists.
  6. Verified that the Kanban board reflects the updated state group ordering.

References

Fixes #8743

Before changing the order

Screenshot 2026-03-10 234449 Screenshot 2026-03-10 234510 ##after cchanging the order Screenshot 2026-03-10 234520 Screenshot 2026-03-10 234546

Video

HEHE.-.Work.items.-.Google.Chrome.2026-03-10.23-45-56.mp4

Summary by CodeRabbit

Release Notes

  • New Features

    • Drag-and-drop reordering of state groups is now available in projects
    • Custom state group ordering is automatically saved and consistently applied across the application
  • Improvements

    • Enhanced state group management interface with visual drag-and-drop indicators

Copilot AI review requested due to automatic review settings March 10, 2026 18:29
@CLAassistant
Copy link

CLAassistant commented Mar 10, 2026

CLA assistant check
All committers have signed the CLA.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 10, 2026

📝 Walkthrough

Walkthrough

This change introduces the ability to customize the display order of project states. It adds a state_group_order field to the Project model, updates serializers and type definitions, implements drag-and-drop UI components for state reordering, and modifies store logic to apply custom ordering when displaying states.

Changes

Cohort / File(s) Summary
Backend Model & Database
apps/api/plane/db/models/project.py, apps/api/plane/db/migrations/0121_project_state_group_order.py
Added state_group_order JSONField to Project model with a default function returning ["backlog", "unstarted", "started", "completed", "cancelled"]. New migration creates the database column.
Backend Serialization & Types
apps/api/plane/api/serializers/project.py, packages/types/src/project/projects.ts
Extended ProjectCreateSerializer to include state_group_order field. Added optional state_group_order property to IProject interface.
Frontend Drag-and-Drop UI
apps/web/core/components/project-states/group-item.tsx, apps/web/core/components/project-states/group-list.tsx
Implemented drag-and-drop reordering for state groups using pragmatic-drag-and-drop. Added visual indicators (DropIndicator), drag state tracking, and API calls to persist order via updateProject with state_group_order.
Frontend Logic & Store
apps/web/core/components/issues/issue-layouts/utils.tsx, apps/web/core/store/state.store.ts
Updated groupedProjectStates getter to respect custom state_group_order when ordering state groups. Refactored utils.tsx with parameter renames and added state sorting logic in getStateColumns using the project's custom order.

Sequence Diagram

sequenceDiagram
    participant User as User
    participant UI as GroupList/GroupItem
    participant Store as State Store
    participant API as Backend API
    participant DB as Database

    User->>UI: Drag state group to reorder
    UI->>UI: Update local drag state (isDragging, closestEdge)
    UI->>UI: Render DropIndicator at drop position
    User->>UI: Drop state group
    UI->>Store: Call updateProject with new state_group_order
    Store->>API: POST/PATCH request with state_group_order
    API->>DB: Save new state_group_order to Project
    DB-->>API: Confirm save
    API-->>Store: Return updated Project
    Store->>Store: Update groupedProjectStates with custom order
    Store-->>UI: Notify state change
    UI->>UI: Re-render states in new order
    UI-->>User: Display reordered states
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Poem

🐰 A rabbit's reordering rhyme:
State groups once fixed in their place,
Now dance with a drag-and-drop grace!
Custom orders bloom,
No more "backlog" gloom—
Workflows now flow at their pace! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main feature added: custom state group ordering, and references the linked issue #8743.
Description check ✅ Passed The description provides detailed context, addresses all required template sections, includes test scenarios, screenshots, and a video demonstrating the feature.
Linked Issues check ✅ Passed The PR fully implements the feature requested in #8743: custom state group ordering is now persistent via the database, drag-and-drop enables reordering on the States settings page, and the Kanban board reflects the custom order.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing custom state group ordering. Backend storage, API serialization, frontend UI/UX, and state management updates are all in scope for the requested feature.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


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
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds per-project customization of the display order for state groups, persisted on the backend and applied by the web app when grouping states (including drag-and-drop reordering in Project Settings → States).

Changes:

  • Add state_group_order to the Project model + migration and expose it via project serializers/types.
  • Apply state_group_order when grouping project states in the frontend store.
  • Add drag-and-drop reordering of state groups in the project states settings UI and apply ordering in issue layout utilities.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/types/src/project/projects.ts Extends IProject with state_group_order for typed consumption in web.
apps/web/core/store/state.store.ts Uses project state_group_order to build an ordered groupedProjectStates map.
apps/web/core/components/project-states/group-list.tsx Adds global DnD monitoring to persist group reordering via updateProject.
apps/web/core/components/project-states/group-item.tsx Makes each group draggable/droppable and shows drop indicators.
apps/web/core/components/issues/issue-layouts/utils.tsx Sorts state columns using the project’s state_group_order when present.
apps/api/plane/db/models/project.py Adds state_group_order JSONField with a default order.
apps/api/plane/db/migrations/0121_project_state_group_order.py Migration to add state_group_order column to projects.
apps/api/plane/api/serializers/project.py Exposes state_group_order via create/update serializers.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +81 to +83
const sourceId = source.data.id as string;
const destinationId = destination.data.id as string;
const edge = extractClosestEdge(destination.data);
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

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

monitorForElements only checks the drag source type, but onDrop assumes the first dropTarget is a STATE_GROUP and blindly reads destination.data.id. If a STATE_GROUP is dropped over a nested/non-group drop target (e.g. a state row), destination.data.id will be a state id or undefined and the reorder calculation will produce an incorrect newOrder (or splice at -1). Add a guard to ensure the destination drop target has data.type === "STATE_GROUP" (and a valid id) before computing the new order.

Suggested change
const sourceId = source.data.id as string;
const destinationId = destination.data.id as string;
const edge = extractClosestEdge(destination.data);
const destinationData = destination.data;
if (
!destinationData ||
destinationData.type !== "STATE_GROUP" ||
!destinationData.id
) {
return;
}
const sourceId = source.data.id as string;
const destinationId = destinationData.id as string;
const edge = extractClosestEdge(destinationData);

Copilot uses AI. Check for mistakes.

// API call
if (workspaceSlug && projectId) {
updateProject(workspaceSlug.toString(), projectId.toString(), { state_group_order: newOrder as TStateGroups[] });
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

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

updateProject(...) returns a Promise and is currently fired-and-forgotten inside onDrop. If the request fails, this can lead to an unhandled promise rejection and no user feedback. Consider explicitly handling the promise (e.g., void updateProjectPromise.catch(...) / setPromiseToast(...)) so failures are surfaced and don’t pollute the console.

Suggested change
updateProject(workspaceSlug.toString(), projectId.toString(), { state_group_order: newOrder as TStateGroups[] });
const updatePromise = updateProject(
workspaceSlug.toString(),
projectId.toString(),
{ state_group_order: newOrder as TStateGroups[] }
);
void updatePromise.catch((error) => {
console.error("Failed to update project state group order", error);
});

Copilot uses AI. Check for mistakes.
Comment on lines +222 to +228
sortedStates = [..._states].sort((a, b) => {
const aGroupIndex = stateGroupOrder.indexOf(a.group);
const bGroupIndex = stateGroupOrder.indexOf(b.group);
if (aGroupIndex !== bGroupIndex) {
return aGroupIndex - bGroupIndex;
}
return a.sequence - b.sequence;
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

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

The sort comparator uses stateGroupOrder.indexOf(...) directly. If state_group_order is ever missing a group (or contains an unknown group), indexOf returns -1 and those states will sort before all valid groups, producing a surprising order. Treat missing groups as “after all known groups” (or fall back to the default STATE_GROUPS order) so corrupted/partial data doesn’t break ordering.

Copilot uses AI. Check for mistakes.
Comment on lines 71 to 96
class Meta:
model = Project
fields = [
"name",
"description",
"project_lead",
"default_assignee",
"identifier",
"icon_prop",
"emoji",
"cover_image",
"module_view",
"cycle_view",
"issue_views_view",
"page_view",
"intake_view",
"guest_view_all_features",
"archive_in",
"close_in",
"timezone",
"external_source",
"external_id",
"is_issue_type_enabled",
"is_time_tracking_enabled",
"state_group_order",
]
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

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

state_group_order is now writable via ProjectCreateSerializer/ProjectUpdateSerializer, but there’s no validation to ensure it’s a list of allowed state group keys, contains no duplicates, and (optionally) includes all required groups. Without this, clients can persist arbitrary JSON that will later break ordering logic in the UI. Add a validate_state_group_order (or extend validate) to enforce the allowed set (backlog/unstarted/started/completed/cancelled) and normalize/fill missing values if needed.

Copilot uses AI. Check for mistakes.
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.

🧹 Nitpick comments (5)
apps/web/core/components/project-states/group-list.tsx (2)

70-108: Potential edge case when destination not found in order.

If destinationId is not in newOrder (e.g., due to a race condition or stale state), indexOf returns -1. With edge === "bottom", this becomes 0, and the source is inserted at the beginning. While unlikely, this could produce unexpected ordering.

Consider adding a guard:

Suggested guard for edge case
 // Find insert index
 let insertIndex = newOrder.indexOf(destinationId);
+if (insertIndex === -1) return; // Destination not found, abort reorder
 if (edge === "bottom") {
   insertIndex += 1;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/core/components/project-states/group-list.tsx` around lines 70 -
108, The onDrop handler in monitorForElements can insert the source at an
incorrect place if destinationId is missing from newOrder; inside the onDrop (in
group-list.tsx) check the result of newOrder.indexOf(destinationId) and handle
-1 explicitly (either abort the reorder or append to the end) before adjusting
for edge === "bottom" and calling updateProject; update the logic around
insertIndex/newOrder (references: onDrop, sourceId, destinationId, newOrder,
insertIndex, edge, updateProject) to guard against stale/missing destination
IDs.

103-105: No error handling for updateProject failure.

If the updateProject API call fails, the UI will have already visually reordered (via the computed property reading from groupedStates), but the backend state won't match. Consider adding error handling to show a toast or revert the visual state on failure.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/core/components/project-states/group-list.tsx` around lines 103 -
105, The UI reorders locally using groupedStates but doesn’t handle failures
from updateProject; wrap the call to updateProject (the place where
workspaceSlug, projectId and newOrder as TStateGroups[] are used) in an async
try/catch, await the promise, and on error revert the local reorder (reset
groupedStates to previous order or trigger a refetch) and show a user-facing
error (e.g., toast.error) so backend/state stay consistent; ensure you capture
the previous order before mutating so you can restore it on failure.
apps/api/plane/api/serializers/project.py (1)

95-95: Consider adding validation for state_group_order in the serializer.

The field is exposed without input validation. Malformed payloads (e.g., invalid group names, duplicates, wrong data types) could be persisted. Consider adding a validate_state_group_order method to ensure only valid state group values are accepted.

Example validation approach
VALID_STATE_GROUPS = {"backlog", "unstarted", "started", "completed", "cancelled"}

def validate_state_group_order(self, value):
    if not isinstance(value, list):
        raise serializers.ValidationError("state_group_order must be a list")
    if not all(isinstance(item, str) and item in VALID_STATE_GROUPS for item in value):
        raise serializers.ValidationError(f"state_group_order must contain only valid state groups: {VALID_STATE_GROUPS}")
    return value
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/plane/api/serializers/project.py` at line 95, Add a field-level
validator method validate_state_group_order to the serializer class that ensures
state_group_order is a list of strings, each string is one of the allowed groups
(e.g., VALID_STATE_GROUPS =
{"backlog","unstarted","started","completed","cancelled"}), rejects duplicates,
and raises serializers.ValidationError with a clear message on type/invalid
value/duplicate detection; implement this method on the serializer that declares
the "state_group_order" field so Django REST Framework will invoke it during
validation.
apps/api/plane/db/models/project.py (1)

118-118: Consider adding validation for state_group_order values.

The JSONField accepts any JSON-serializable data. If invalid state group names are saved (e.g., typos or non-existent groups), the frontend sorting logic will silently ignore them. You may want to add model-level or serializer-level validation to ensure only valid state groups (backlog, unstarted, started, completed, cancelled) are stored.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/plane/db/models/project.py` at line 118, Add validation to ensure
state_group_order contains only allowed group names by validating before save:
in the Project model (where state_group_order =
models.JSONField(default=get_default_state_group_order)) add a model-level
clean() or a JSONField validator that checks the value is a list/array and every
entry is one of the allowed set
{'backlog','unstarted','started','completed','cancelled'} and raise
django.core.exceptions.ValidationError on violation; alternatively add
equivalent validation in the Project serializer (e.g.,
ProjectSerializer.validate_state_group_order) so invalid entries are rejected
before persisting or returning.
apps/web/core/store/state.store.ts (1)

312-318: Verify if pessimistic delete is intentional.

The deleteState method now waits for the API call to succeed before removing the state from local store. This differs from updateState (lines 287-304), which uses optimistic updates with rollback on failure. If the API call is slow or fails, the UI will not reflect the deletion until completion/error.

Consider whether this should follow the same optimistic pattern for consistency:

Optimistic delete with rollback pattern
 deleteState = async (workspaceSlug: string, projectId: string, stateId: string) => {
   if (!this.stateMap?.[stateId]) return;
+  const originalState = this.stateMap[stateId];
+  try {
+    runInAction(() => {
+      delete this.stateMap[stateId];
+    });
     await this.stateService.deleteState(workspaceSlug, projectId, stateId);
-  runInAction(() => {
-    delete this.stateMap[stateId];
-  });
+  } catch (error) {
+    runInAction(() => {
+      this.stateMap[stateId] = originalState;
+    });
+    throw error;
+  }
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/core/store/state.store.ts` around lines 312 - 318, deleteState
currently performs a pessimistic delete (waits for stateService.deleteState to
succeed) which is inconsistent with updateState's optimistic pattern; change
deleteState to perform an optimistic removal by storing a backup of
this.stateMap[stateId], calling runInAction to delete this.stateMap[stateId]
before awaiting stateService.deleteState(workspaceSlug, projectId, stateId), and
on catch/failed API call restore the backup inside runInAction (optionally
logging the error) so the store rolls back on failure; keep the early-return
guard on !this.stateMap?.[stateId] and ensure errors are rethrown or handled
consistently with updateState.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/api/plane/api/serializers/project.py`:
- Line 95: Add a field-level validator method validate_state_group_order to the
serializer class that ensures state_group_order is a list of strings, each
string is one of the allowed groups (e.g., VALID_STATE_GROUPS =
{"backlog","unstarted","started","completed","cancelled"}), rejects duplicates,
and raises serializers.ValidationError with a clear message on type/invalid
value/duplicate detection; implement this method on the serializer that declares
the "state_group_order" field so Django REST Framework will invoke it during
validation.

In `@apps/api/plane/db/models/project.py`:
- Line 118: Add validation to ensure state_group_order contains only allowed
group names by validating before save: in the Project model (where
state_group_order = models.JSONField(default=get_default_state_group_order)) add
a model-level clean() or a JSONField validator that checks the value is a
list/array and every entry is one of the allowed set
{'backlog','unstarted','started','completed','cancelled'} and raise
django.core.exceptions.ValidationError on violation; alternatively add
equivalent validation in the Project serializer (e.g.,
ProjectSerializer.validate_state_group_order) so invalid entries are rejected
before persisting or returning.

In `@apps/web/core/components/project-states/group-list.tsx`:
- Around line 70-108: The onDrop handler in monitorForElements can insert the
source at an incorrect place if destinationId is missing from newOrder; inside
the onDrop (in group-list.tsx) check the result of
newOrder.indexOf(destinationId) and handle -1 explicitly (either abort the
reorder or append to the end) before adjusting for edge === "bottom" and calling
updateProject; update the logic around insertIndex/newOrder (references: onDrop,
sourceId, destinationId, newOrder, insertIndex, edge, updateProject) to guard
against stale/missing destination IDs.
- Around line 103-105: The UI reorders locally using groupedStates but doesn’t
handle failures from updateProject; wrap the call to updateProject (the place
where workspaceSlug, projectId and newOrder as TStateGroups[] are used) in an
async try/catch, await the promise, and on error revert the local reorder (reset
groupedStates to previous order or trigger a refetch) and show a user-facing
error (e.g., toast.error) so backend/state stay consistent; ensure you capture
the previous order before mutating so you can restore it on failure.

In `@apps/web/core/store/state.store.ts`:
- Around line 312-318: deleteState currently performs a pessimistic delete
(waits for stateService.deleteState to succeed) which is inconsistent with
updateState's optimistic pattern; change deleteState to perform an optimistic
removal by storing a backup of this.stateMap[stateId], calling runInAction to
delete this.stateMap[stateId] before awaiting
stateService.deleteState(workspaceSlug, projectId, stateId), and on catch/failed
API call restore the backup inside runInAction (optionally logging the error) so
the store rolls back on failure; keep the early-return guard on
!this.stateMap?.[stateId] and ensure errors are rethrown or handled consistently
with updateState.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: feb7afd5-a5a7-4e87-a7cd-e409d2086806

📥 Commits

Reviewing files that changed from the base of the PR and between 6627282 and f950d6f.

📒 Files selected for processing (8)
  • apps/api/plane/api/serializers/project.py
  • apps/api/plane/db/migrations/0121_project_state_group_order.py
  • apps/api/plane/db/models/project.py
  • apps/web/core/components/issues/issue-layouts/utils.tsx
  • apps/web/core/components/project-states/group-item.tsx
  • apps/web/core/components/project-states/group-list.tsx
  • apps/web/core/store/state.store.ts
  • packages/types/src/project/projects.ts

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.

🚀 Feature: Ability to change display order of statuses

3 participants