Skip to content

Conversation

@anton-v-a
Copy link

@anton-v-a anton-v-a commented Jan 27, 2026

Pull Request: Calendar and Kanban Layouts for Workspace Views

Summary

This PR adds Calendar and Kanban layout support to workspace-level views in Plane. Previously, workspace views (All Issues, Assigned, Created, Subscribed) only supported the Spreadsheet layout. Users can now visualize their work across all projects in time-based (Calendar) and workflow-based (Kanban) formats.

Key Features

  • Calendar Layout: View issues grouped by target date with month/week navigation
  • Kanban Layout: View issues grouped by state group, priority, project, or labels
  • "No Date" Section: Collapsible section in Calendar showing issues without a target date
  • Drag-and-Drop: Move issues between dates (Calendar) or columns (Kanban)
  • Quick Add: Create issues directly from Calendar/Kanban with project selection
  • Cross-Project State Mapping: Intelligent state resolution when moving issues between state groups

Changes Overview

Frontend (apps/web)

New Components

File Description
calendar/roots/workspace-root.tsx Workspace Calendar root component with "No Date" section support
kanban/roots/workspace-root.tsx Workspace Kanban root component with state group mapping
quick-add/workspace-root.tsx Quick add component with project dropdown for workspace views

Modified Components

File Changes
calendar/calendar.tsx Added "No Date" collapsible section, updated props for workspace support
roots/all-issue-layout-root.tsx Integrated Calendar and Kanban layout rendering
utils.tsx Added findStateByGroup() utility for cross-project state mapping, enhanced drag-drop handling for state_detail.group
issue-layout-HOC.tsx Updated layout type handling

Store Changes

File Changes
workspace/filter.store.ts Auto-set state_detail.group as default group_by for Kanban, layout switch handling
workspace/issue.store.ts Added grouped pagination support, undefined response handling for aborted requests
helpers/base-issues.store.ts Added addIssue() method exposure for issue map updates

Hooks & Services

File Changes
use-issues.ts Exposed addIssuesToMap function for adding fetched issues to the store
workspace.service.ts Updated return type to handle undefined (aborted requests)

Backend (apps/api)

API Changes

File Changes
views/view/base.py Added grouped pagination to WorkspaceViewIssuesViewSet, backward-compatible with existing clients
utils/filters/filterset.py Added target_date__isnull and start_date__isnull filters for "No Date" queries

Packages

Constants (packages/constants)

File Changes
issue/filter.ts Added WORKSPACE_KANBAN_GROUP_BY_OPTIONS, updated layout configs for my_issues filter type
issue/common.ts Added "calendar" to WORKSPACE_ACTIVE_LAYOUTS

Technical Implementation Details

1. State Group to State ID Mapping

Workspace views group issues by state_detail.group (e.g., "backlog", "started", "completed") instead of specific state_id because states are project-specific. When an issue is dragged to a new state group column:

  1. The findStateByGroup() utility finds a matching state in the issue's project
  2. Prefers the default state for that group, falls back to first match
  3. Updates the issue's state_id to the resolved state
export const findStateByGroup = (
  projectStates: IState[] | undefined,
  targetStateGroup: string
): IState | undefined => {
  if (!projectStates) return undefined;
  return (
    projectStates.find((s) => s.group === targetStateGroup && s.default) ||
    projectStates.find((s) => s.group === targetStateGroup)
  );
};

2. "No Date" Section Architecture

The Calendar view fetches issues without target_date separately from date-range issues:

  • Separate API call: Uses target_date__isnull=true filter
  • Independent state: Managed via noDateIssueIds and noDateTotalCount
  • Cached results: Only re-fetches when appliedFiltersKey changes
  • Collapsible UI: Users can show/hide the section

3. Grouped Pagination for Kanban

The backend WorkspaceViewIssuesViewSet now supports grouped pagination:

  • Without group_by: Returns flat list (backward compatible)
  • With group_by: Returns grouped structure with per-group pagination
  • Uses existing GroupedOffsetPaginator from project issue views

4. Quick Add with Project Selection

Workspace views require project selection before creating an issue:

  • Project dropdown appears in quick add form
  • State is auto-resolved based on the column's state group
  • Pre-populated data (target_date, priority, etc.) is applied

API Changes

WorkspaceViewIssuesViewSet

Endpoint: GET /api/v1/workspaces/{workspace_slug}/views/issues/

New Query Parameters:

Parameter Type Description
group_by string Group results by field (e.g., state_detail.group, priority)
sub_group_by string Sub-group results (optional)
target_date__isnull boolean Filter issues without target date

Backward Compatibility: ✅ Existing clients receive flat list when group_by is not provided.


Testing Checklist

Calendar Layout

  • Month view displays issues on correct dates
  • Week view navigation works correctly
  • "No Date" section shows only issues without target_date
  • "No Date" section collapses/expands
  • Drag-drop changes issue target_date
  • Filters apply correctly across all projects

Kanban Layout

  • Groups by state_detail.group correctly
  • Groups by priority correctly
  • Groups by project correctly
  • Groups by labels correctly
  • Drag-drop between state groups updates issue state
  • Empty groups show/hide based on display filter

Cross-cutting

  • Layout persistence works (switching between layouts remembers choice)
  • Loading states display correctly during layout switch
  • Aborted requests don't cause errors

Related Issues


Migration Notes

No database migrations required. The feature uses existing issue fields and adds optional query parameters to existing endpoints.


Rollback Plan

  1. Revert this PR
  2. No database changes to rollback
  3. Users will see only Spreadsheet layout in workspace views (previous behavior)

Summary by CodeRabbit

Release Notes

  • New Features

    • Added Calendar view for workspace-level issue management with dedicated section for issues without dates
    • Introduced Kanban view for workspace-level issue organization with state-based grouping
    • Enhanced quick-issue creation to work across workspace with project selection
    • Added support for grouping issues by state and multiple grouping levels with improved pagination
  • Bug Fixes

    • Fixed null date filtering for target and start date fields
  • Improvements

    • Streamlined layout selection interface for better view management
    • Enhanced drag-and-drop functionality across calendar and kanban layouts
    • Optimized issue fetching for calendar and kanban views based on layout type

✏️ Tip: You can customize this high-level summary in your review settings.

@CLAassistant
Copy link

CLAassistant commented Jan 27, 2026

CLA assistant check
All committers have signed the CLA.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 27, 2026

📝 Walkthrough

Walkthrough

This PR implements workspace-level calendar and kanban views for individual users across projects. It adds grouped pagination support to the backend API, introduces new workspace root components for calendar and kanban layouts, consolidates filter store types across calendar components, and provides utilities for state grouping and drag-drop interactions.

Changes

Cohort / File(s) Summary
Backend Pagination & Filtering
apps/api/plane/app/views/view/base.py, apps/api/plane/utils/filters/filterset.py
Added grouped pagination support with GroupedOffsetPaginator and SubGroupedOffsetPaginator to WorkspaceViewIssuesViewSet.list; validates that group_by and sub_group_by are not identical; added target_date__isnull and start_date__isnull BooleanFilter entries for date null-handling.
Type System Consolidation (Calendar Components)
apps/web/core/components/issues/issue-layouts/calendar/calendar.tsx, day-tile.tsx, dropdowns/months-dropdown.tsx, dropdowns/options-dropdown.tsx, header.tsx, week-days.tsx
Consolidated multiple specific issue filter type imports (IProjectIssuesFilter, IModuleIssuesFilter, etc.) into a single IBaseIssueFilterStore type across calendar components; updated issuesFilterStore prop signatures and header method signature for updateFilters to include filters: TSupportedFilterForUpdate parameter.
Calendar Workspace Implementation
apps/web/core/components/issues/issue-layouts/calendar/roots/workspace-root.tsx
New component WorkspaceCalendarRoot that manages workspace-level calendar views with permission-based capabilities, no-date issue handling, drag-and-drop between dates, and integration with issue stores and quick-add actions.
Kanban Workspace Implementation
apps/web/core/components/issues/issue-layouts/kanban/roots/workspace-root.tsx
New component WorkspaceKanBanRoot that orchestrates workspace-level Kanban views with auto-scroll support, delete-drop area with confirmation, per-project permission checks, and group collapsing/expanding.
Quick-Add Workspace Implementation
apps/web/core/components/issues/issue-layouts/quick-add/workspace-root.tsx, index.ts, root.tsx
New component WorkspaceQuickAddIssueRoot for workspace-scoped quick-add UI with project selection, state resolution via findStateByGroup, and form handling with toast-based feedback; routing updated to support workspace views.
Helper & Utility Updates
apps/web/ce/components/views/helper.tsx, apps/web/core/components/issues/issue-layouts/utils.tsx, all-issue-layout-root.tsx, issue-layout-HOC.tsx
New layout selection components (GlobalViewLayoutSelection, WorkspaceAdditionalLayouts); added findStateByGroup utility for state group mapping in drag-drop handling; updated fetch logic with layout-aware behavior for calendar and kanban.
Store & Hooks Extensions
apps/web/core/store/issue/helpers/base-issues.store.ts, workspace/filter.store.ts, workspace/issue.store.ts, apps/web/core/hooks/store/use-issues.ts, use-group-dragndrop.ts, use-issues-actions.tsx
Added clearIssueIds() and setLoader() methods to IBaseIssuesStore; changed updateFilterExpression and updateFilters to synchronous in workspace filter store with layout-aware fetch logic; added archiveIssue and quickAddIssue to global store surface; extended DNDStoreType to include GLOBAL; added addIssuesToMap() to default issue store.
Service & Constants Updates
apps/web/core/services/workspace.service.ts, apps/live/src/services/page/core.service.ts, packages/constants/src/issue/...
Updated getViewIssues return type to include undefined with abort handling; renamed TPageDescriptionPayload.description to description_json; added WORKSPACE_KANBAN_GROUP_BY_OPTIONS constant and expanded ISSUE_DISPLAY_FILTERS_BY_PAGE with calendar/kanban/spreadsheet/gantt_chart entries; added state_detail.group to DRAG_ALLOWED_GROUPS.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant WorkspaceCalendarRoot
    participant IssueStore
    participant WorkspaceService
    participant CalendarChart
    participant DragDropHandler

    User->>WorkspaceCalendarRoot: Load workspace calendar view
    WorkspaceCalendarRoot->>IssueStore: Fetch calendar issues by target_date
    IssueStore->>WorkspaceService: GET issues (grouped by target_date)
    WorkspaceService-->>IssueStore: Return grouped issues
    WorkspaceCalendarRoot->>IssueStore: Fetch no-date issues separately
    IssueStore->>WorkspaceService: GET issues (target_date__isnull=true)
    WorkspaceService-->>IssueStore: Return no-date issues
    WorkspaceCalendarRoot->>CalendarChart: Render with grouped data + no-date section
    CalendarChart-->>User: Display calendar with date-grouped issues
    
    User->>DragDropHandler: Drag issue to different date
    DragDropHandler->>WorkspaceCalendarRoot: handleDragDrop(issue, targetDate)
    WorkspaceCalendarRoot->>IssueStore: Update issue.target_date
    IssueStore->>WorkspaceService: PATCH issue with new target_date
    WorkspaceService-->>IssueStore: Confirm update
    IssueStore-->>CalendarChart: Update UI reactively
Loading
sequenceDiagram
    participant User
    participant WorkspaceKanBanRoot
    participant IssueStore
    participant WorkspaceService
    participant KanBanComponent
    participant DeleteDropHandler

    User->>WorkspaceKanBanRoot: Load workspace kanban view
    WorkspaceKanBanRoot->>IssueStore: Fetch issues (grouped by state_detail.group)
    IssueStore->>WorkspaceService: GET issues (group_by=state_detail.group)
    WorkspaceService-->>IssueStore: Return grouped issues
    WorkspaceKanBanRoot->>KanBanComponent: Render with grouped data & group-collapse state
    KanBanComponent-->>User: Display kanban with state groups
    
    User->>KanBanComponent: Drag issue to delete drop zone
    KanBanComponent->>DeleteDropHandler: onDrop(issue)
    DeleteDropHandler->>User: Show DeleteIssueModal
    User->>DeleteDropHandler: Confirm delete
    DeleteDropHandler->>IssueStore: Remove issue
    IssueStore->>WorkspaceService: DELETE issue
    WorkspaceService-->>IssueStore: Confirm deletion
    IssueStore-->>KanBanComponent: Update UI, remove card
Loading
sequenceDiagram
    participant User
    participant WorkspaceQuickAddIssueRoot
    participant ProjectDropdown
    participant QuickAddForm
    participant WorkspaceService
    participant IssueStore
    participant Toast

    User->>WorkspaceQuickAddIssueRoot: Click quick-add trigger
    WorkspaceQuickAddIssueRoot->>WorkspaceQuickAddIssueRoot: Set isOpen=true, select first joined project
    WorkspaceQuickAddIssueRoot->>ProjectDropdown: Display project selector
    User->>ProjectDropdown: Select project
    WorkspaceQuickAddIssueRoot->>WorkspaceService: getProjectStates(selectedProject)
    WorkspaceService-->>WorkspaceQuickAddIssueRoot: Return states
    WorkspaceQuickAddIssueRoot->>QuickAddForm: Resolve state_detail.group to state_id via findStateByGroup
    User->>QuickAddForm: Fill form & submit
    WorkspaceQuickAddIssueRoot->>WorkspaceService: createIssue(projectId, issuePayload)
    WorkspaceService-->>WorkspaceQuickAddIssueRoot: Return created issue
    WorkspaceQuickAddIssueRoot->>IssueStore: addIssuesToMap(issue)
    WorkspaceQuickAddIssueRoot->>Toast: Show success with CreateIssueToastActionItems
    Toast-->>User: Success notification with action items
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

The PR introduces multiple substantial new components (calendar/kanban workspace roots with 287+289 lines of intricate logic involving permissions, drag-drop, and state management), consolidates type systems across numerous files, extends stores and hooks with new methods, and includes significant utilities for state grouping. The heterogeneity—spanning backend pagination, frontend components, stores, hooks, and utilities—combined with logic density in the workspace root components necessitates detailed review of interactions across multiple systems.

Possibly related PRs

Suggested labels

ready to merge, feature

Suggested reviewers

  • prateekshourya29
  • aaryan610

Poem

🐰 Across the workspace, calendars bloom,
Kanban boards dance without a gloom,
State groups mapped, no-dates in sight,
Quick-add sprouts with filtered light,
Users soar through project-wide flight! 🚀

🚥 Pre-merge checks | ✅ 3 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title partially describes the PR's main objective but is truncated and vague. It mentions Calendar/Kanban View for 'Individu…' (Individual User) and references #6642, which aligns with the feature request, but lacks clarity due to truncation and doesn't convey the complete scope of 'workspace-level views'. Consider revising the title to be more descriptive and complete, e.g., 'Add Calendar and Kanban layouts to workspace views' or 'Implement Calendar/Kanban views for workspace-level issues'.
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The PR description is comprehensive and well-structured. It includes detailed summaries of changes, technical implementation details, API changes, testing checklist, and migration notes. It follows best practices and aligns well with the repository's expected documentation standards.
Linked Issues check ✅ Passed The code changes comprehensively implement the objectives from issue #6642: Calendar and Kanban layouts for workspace views are added with month/week navigation, state group mapping, drag-and-drop, and quick-add; cross-project issue visibility is enabled; task timelines and occupancy visibility across projects are provided.
Out of Scope Changes check ✅ Passed All code changes are within scope of adding Calendar and Kanban layouts to workspace views. Changes include layout components, store updates, filter additions, utilities, and hooks—all supporting the core feature. No unrelated refactoring or breaking changes outside the feature objectives were detected.

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

✨ Finishing touches
  • 📝 Generate docstrings

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

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

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

⚠️ Outside diff range comments (4)
apps/web/core/hooks/use-issues-actions.tsx (1)

687-699: Keep pagination viewId consistent with fetchIssues override.

fetchIssues now accepts an explicit viewId, but fetchNextIssues still uses the router globalViewId. If callers pass a viewId (workspace-level views), pagination can no-op or page the wrong list. Consider mirroring the same effectiveViewId logic in fetchNextIssues.

💡 Suggested fix
-  const fetchNextIssues = useCallback(
-    async (groupId?: string, subGroupId?: string) => {
-      if (!workspaceSlug || !globalViewId) return;
-      return issues.fetchNextIssues(workspaceSlug.toString(), globalViewId.toString(), groupId, subGroupId);
-    },
-    [issues.fetchIssues, workspaceSlug, globalViewId]
-  );
+  const fetchNextIssues = useCallback(
+    async (groupId?: string, subGroupId?: string, viewId?: string) => {
+      const effectiveViewId = viewId ?? globalViewId;
+      if (!workspaceSlug || !effectiveViewId) return;
+      return issues.fetchNextIssues(workspaceSlug.toString(), effectiveViewId.toString(), groupId, subGroupId);
+    },
+    [issues.fetchIssues, workspaceSlug, globalViewId]
+  );
-  fetchNextIssues: (groupId?: string, subGroupId?: string) => Promise<TIssuesResponse | undefined>;
+  fetchNextIssues: (groupId?: string, subGroupId?: string, viewId?: string) => Promise<TIssuesResponse | undefined>;
apps/web/core/services/workspace.service.ts (1)

266-283: Fix type annotation in workspace-root.tsx for getViewIssues response.

The getViewIssues method now returns Promise<TIssuesResponse | undefined> for canceled requests, but workspace-root.tsx line 133 declares the response as const response: TIssuesResponse (missing | undefined). This will cause type errors in strict mode. Update the type to TIssuesResponse | undefined or remove the explicit type annotation to infer it correctly. The runtime guard if (response && response.results) is present but the type declaration is incorrect.

apps/web/core/store/issue/workspace/filter.store.ts (1)

248-265: Re-check sub_group_by after normalizing group_by.
If group_by is forced to "state_detail.group", sub_group_by can still equal it, bypassing the earlier guard. Consider validating again after the normalization.

🔧 Suggested fix
          if (_filters.displayFilters.layout === "kanban") {
            if (
              !_filters.displayFilters.group_by ||
              !WORKSPACE_KANBAN_GROUP_BY_OPTIONS.includes(
                _filters.displayFilters.group_by as typeof WORKSPACE_KANBAN_GROUP_BY_OPTIONS[number]
              )
            ) {
              _filters.displayFilters.group_by = "state_detail.group";
              updatedDisplayFilters.group_by = "state_detail.group";
            }
+           if (_filters.displayFilters.group_by === _filters.displayFilters.sub_group_by) {
+             _filters.displayFilters.sub_group_by = null;
+             updatedDisplayFilters.sub_group_by = null;
+           }
          }
apps/web/core/components/issues/issue-layouts/calendar/day-tile.tsx (1)

77-123: Avoid using ref .current in useEffect dependency array.

dayTileRef?.current in the dependency array won't trigger re-renders when the ref changes since refs are mutable and don't cause component updates. The ref object itself (dayTileRef) is stable across renders.

Proposed fix
-  }, [dayTileRef?.current, formattedDatePayload]);
+  }, [formattedDatePayload, handleDragAndDrop, issues]);

Note: You may also want to include handleDragAndDrop and issues in the dependency array since they're used inside the effect, or wrap them in useCallback/memoize appropriately to prevent stale closures.

🤖 Fix all issues with AI agents
In `@apps/api/plane/app/views/view/base.py`:
- Around line 246-247: Replace the unsafe deepcopy of the Django QuerySet:
instead of using copy.deepcopy(issue_queryset) to create
filtered_issue_queryset, call issue_queryset.all() to produce a new, independent
QuerySet (and remove the now-unused import copy from the top of the file if it's
no longer referenced). Ensure this change targets the filtered_issue_queryset
assignment where issue_queryset is referenced.

In
`@apps/web/core/components/issues/issue-layouts/calendar/roots/workspace-root.tsx`:
- Around line 103-165: The no-date fetch (fetchNoDateIssues) currently uses
perPageCount: 50 and sets setNoDateTotalCount(issueIds.length), which caps and
misreports totals; change it to read TIssuesResponse.total_count for total count
and implement pagination/load-more using the same pattern as the main calendar
(use workspaceService.getViewIssues response.next_page_results / cursors and a
loadMoreNoDateIssues handler or reuse loadMoreIssues) to request additional
pages instead of relying on a single 50-item request; accumulate results by
appending new issues to the existing no-date IDs (setNoDateIssueIds) and calling
addIssuesToMap for each page, and only fall back to client-side filtering of
issue.target_date while preserving the API pagination cursors rather than
truncating by array.length.

In
`@apps/web/core/components/issues/issue-layouts/kanban/roots/workspace-root.tsx`:
- Around line 205-218: handleCollapsedGroups currently mutates the store-backed
collapsedGroups array with push(), which can cause non-atomic updates; instead
create a new array before calling updateFilters: read the existing array from
issuesFilter?.issueFilters?.kanbanFilters?.[toggle] into collapsedGroups, then
if value is included produce a new array using filter to remove it, otherwise
produce a new array by concatenating the value (e.g., [...collapsedGroups,
value]); pass that new array to updateFilters (function updateFilters) so the
original store array is never mutated in place.
- Around line 190-203: The current handleDeleteIssue swallows errors by catching
all exceptions and always resolving, preventing DeleteIssueModal from receiving
rejections and showing error toasts; update handleDeleteIssue so that you await
removeIssue(draggedIssue.project_id, draggedIssueId) and only call
setDeleteIssueModal(false) and setDraggedIssueId(undefined) on success, but do
not swallow failures—either remove the try/catch entirely or rethrow the caught
error in the catch block (keep references to handleDeleteIssue, removeIssue,
setDeleteIssueModal, setDraggedIssueId, and DeleteIssueModal to locate the
change).

In `@apps/web/core/components/issues/issue-layouts/quick-add/workspace-root.tsx`:
- Around line 69-87: The logic in resolvedPrePopulatedData (useMemo) currently
retains "state_detail.group" when no matching state is found; update it so you
always remove the "state_detail.group" key from prePopulatedData and only add
state_id when findStateByGroup(projectStates, stateGroup) returns a targetState;
locate the resolution in resolvedPrePopulatedData (references:
selectedProjectId, prePopulatedData, getProjectStates, findStateByGroup, TIssue)
and return the spread rest without the "state_detail.group" field in both
branches, conditionally merging state_id = targetState.id only when targetState
exists.

In `@apps/web/core/components/issues/issue-layouts/utils.tsx`:
- Around line 563-597: When handling groupBy === "state_detail.group", guard
against a missing project state list by checking the result of
getProjectStates(sourceIssue.project_id) and if projectStates is undefined (i.e.
sourceIssue.project_id is falsy or no states returned) throw an explicit Error
(or otherwise block the drop) before calling findStateByGroup; update the logic
around getProjectStates, findStateByGroup, updatedIssue and issueUpdates to
ensure you only set state_id and issueUpdates when a valid targetState is found
and otherwise reject the operation with a clear error message.
🧹 Nitpick comments (5)
apps/web/ce/components/views/helper.tsx (1)

8-12: Unused workspaceSlug prop in GlobalViewLayoutSelection.

The workspaceSlug property is defined in TLayoutSelectionProps but is not destructured or used in the component implementation. If this is intentional for API consistency, consider adding a comment. Otherwise, remove it from the type definition to keep the interface clean.

♻️ Suggested fix if the prop is not needed
 export type TLayoutSelectionProps = {
   onChange: (layout: EIssueLayoutTypes) => void;
   selectedLayout: EIssueLayoutTypes;
-  workspaceSlug: string;
 };

Also applies to: 21-22

apps/api/plane/app/views/view/base.py (1)

266-339: Add validation for allowed group_by and sub_group_by field values.

The code correctly prevents group_by and sub_group_by from being equal, but does not validate that these values are from the set of supported grouping fields (state_id, priority, state__group, cycle_id, project_id, labels__id, assignees__id, issue_module__module_id, target_date, start_date, created_by). Invalid field names are silently ignored—issue_group_values returns an empty list and results fail to group properly—leaving users without feedback that their grouping parameter was unsupported. Adding validation to reject invalid field names with a 400 error would improve clarity and user experience.

apps/web/core/components/issues/issue-layouts/calendar/calendar.tsx (1)

241-276: Add ARIA state for the “No Date” toggle.

This makes the collapsible section discoverable to screen readers.

♿ Proposed accessibility tweak
-                <button
+                <button
                   type="button"
+                  aria-expanded={!isNoDateCollapsed}
+                  aria-controls="no-date-section"
                   className="flex w-full items-center gap-2 px-4 py-2 bg-layer-1 cursor-pointer hover:bg-layer-2 text-left"
                   onClick={() => setIsNoDateCollapsed(!isNoDateCollapsed)}
                 >
                   <ChevronRight
                     className={cn("size-4 text-tertiary transition-transform", {
                       "rotate-90": !isNoDateCollapsed,
                     })}
                   />
                   <span className="text-13 font-medium text-secondary">No Date</span>
                   <span className="text-11 text-tertiary">({noDateIssueCount ?? noDateIssueIds.length})</span>
                 </button>
                 {!isNoDateCollapsed && (
-                  <div className="px-4 py-2 bg-surface-1">
+                  <div id="no-date-section" className="px-4 py-2 bg-surface-1">
                     <CalendarIssueBlocks
                       date={new Date()}
                       issueIdList={noDateIssueIds}
                       loadMoreIssues={() => {}}
apps/web/core/components/issues/issue-layouts/kanban/roots/workspace-root.tsx (1)

81-92: Avoid duplicate permission scans per render.

Compute once and reuse for the two prop expressions.

♻️ Proposed small refactor
   const canCreateIssues = useCallback(() => {
     if (!joinedProjectIds || joinedProjectIds.length === 0) return false;
     return joinedProjectIds.some((projectId) =>
       allowPermissions(
         [EUserPermissions.ADMIN, EUserPermissions.MEMBER],
         EUserPermissionsLevel.PROJECT,
         workspaceSlug?.toString(),
         projectId
       )
     );
   }, [joinedProjectIds, allowPermissions, workspaceSlug]);

+  const canCreateIssuesValue = canCreateIssues();
...
-                enableQuickIssueCreate={enableQuickAdd && canCreateIssues()}
+                enableQuickIssueCreate={enableQuickAdd && canCreateIssuesValue}
...
-                disableIssueCreation={!enableIssueCreation || !canCreateIssues()}
+                disableIssueCreation={!enableIssueCreation || !canCreateIssuesValue}

Also applies to: 223-223, 270-273

apps/web/core/components/issues/issue-layouts/calendar/roots/workspace-root.tsx (1)

51-62: Compute canCreateIssues once per render.

Avoid repeating the permission scan for both creation props.

♻️ Small reuse improvement
   const canCreateIssues = useCallback(() => {
     if (!joinedProjectIds || joinedProjectIds.length === 0) return false;
     return joinedProjectIds.some((projectId) =>
       allowPermissions(
         [EUserPermissions.ADMIN, EUserPermissions.MEMBER],
         EUserPermissionsLevel.PROJECT,
         workspaceSlug?.toString(),
         projectId
       )
     );
   }, [joinedProjectIds, allowPermissions, workspaceSlug]);

+  const canCreateIssuesValue = canCreateIssues();
...
-        enableQuickIssueCreate={enableQuickAdd && canCreateIssues()}
-        disableIssueCreation={!enableIssueCreation || !canCreateIssues()}
+        enableQuickIssueCreate={enableQuickAdd && canCreateIssuesValue}
+        disableIssueCreation={!enableIssueCreation || !canCreateIssuesValue}

Also applies to: 274-275

Comment on lines +246 to +247
# Keeping a copy of the queryset before applying annotations (for counts)
filtered_issue_queryset = copy.deepcopy(issue_queryset)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid copy.deepcopy on Django QuerySets.

Django QuerySets have internal state (database connections, caches, prefetch state) that may not clone correctly with deepcopy. The idiomatic way to clone a queryset is using .all(), which returns a new QuerySet with the same filters but independent evaluation.

Proposed fix
-        # Keeping a copy of the queryset before applying annotations (for counts)
-        filtered_issue_queryset = copy.deepcopy(issue_queryset)
+        # Keeping a copy of the queryset before applying annotations (for counts)
+        filtered_issue_queryset = issue_queryset.all()

If copy is no longer used elsewhere in this file, you can also remove the import copy at line 1.

🤖 Prompt for AI Agents
In `@apps/api/plane/app/views/view/base.py` around lines 246 - 247, Replace the
unsafe deepcopy of the Django QuerySet: instead of using
copy.deepcopy(issue_queryset) to create filtered_issue_queryset, call
issue_queryset.all() to produce a new, independent QuerySet (and remove the
now-unused import copy from the top of the file if it's no longer referenced).
Ensure this change targets the filtered_issue_queryset assignment where
issue_queryset is referenced.

Comment on lines +103 to +165
// Fetch "No Date" issues (issues without target_date) separately from date-range issues.
// Architecture note: This makes a separate API call from the main calendar fetch.
// This is intentional because:
// 1. Date-range issues need grouping by target_date, no-date issues don't
// 2. Separate calls allow independent pagination and loading states
// 3. Results are cached in local state and only re-fetched when filters change
// If performance becomes a concern, consider batching into a single API call.
// Fire-and-forget: local state updates when fetch completes
useEffect(() => {
if (!workspaceSlug || !globalViewId) return;

const fetchNoDateIssues = async () => {
try {
// Get base params from the filter store for the current view
const baseParams = issuesFilter.getFilterParams(
{ canGroup: false, perPageCount: 50 },
globalViewId,
undefined,
undefined,
undefined
);
// Remove any existing target_date filter to avoid conflicts with target_date__isnull
// The view might have date range filters that would otherwise override our null filter
const { target_date: _existingTargetDate, ...paramsWithoutTargetDate } = baseParams as Record<string, unknown>;
// Add filter for issues without target_date
const params = {
...paramsWithoutTargetDate,
target_date__isnull: "true",
};

const response: TIssuesResponse = await workspaceService.getViewIssues(workspaceSlug.toString(), params);

if (response && response.results) {
const results = response.results;
if (Array.isArray(results)) {
// Type guard to extract TIssue objects and filter to only issues without target_date
// The client-side filter is defensive - API should already filter via target_date__isnull
const issues = results.filter(
(issue: TIssue | string): issue is TIssue =>
typeof issue !== "string" && !!issue.id && !issue.target_date
);
const issueIds = issues.map((issue) => issue.id);
setNoDateIssueIds(issueIds);
setNoDateTotalCount(issueIds.length);

// Add issues to the issue map so they can be displayed
if (issues.length > 0) {
addIssuesToMap(issues);
}
} else {
setNoDateIssueIds([]);
setNoDateTotalCount(0);
}
}
} catch (error) {
console.error("Failed to fetch no-date issues:", error);
setNoDateIssueIds([]);
setNoDateTotalCount(0);
}
};

void fetchNoDateIssues();
}, [workspaceSlug, globalViewId, appliedFiltersKey]);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find TIssuesResponse definition
rg -n "interface TIssuesResponse|type TIssuesResponse" --max-count=5

Repository: makeplane/plane

Length of output: 197


🏁 Script executed:

# Check the workspaceService.getViewIssues method signature
rg -n "getViewIssues" -A 10 | head -60

Repository: makeplane/plane

Length of output: 4777


🏁 Script executed:

# Look for how pagination is handled in other parts of the calendar or issues
rg -n "perPageCount" apps/web/core/components/issues --max-count=10

Repository: makeplane/plane

Length of output: 1260


🏁 Script executed:

# Search for total or count field usage in API responses
rg -n "\.total|\.count|totalCount" apps/web/core/components/issues/issue-layouts/calendar -B 2 -A 2

Repository: makeplane/plane

Length of output: 41


🏁 Script executed:

# Read the TIssuesResponse type definition
cat packages/types/src/issues/issue.ts | head -150 | tail -50

Repository: makeplane/plane

Length of output: 1044


🏁 Script executed:

# Get more context around line 120
sed -n '110,140p' packages/types/src/issues/issue.ts

Repository: makeplane/plane

Length of output: 701


🏁 Script executed:

# Check how setNoDateTotalCount is used elsewhere
rg -n "setNoDateTotalCount|NoDateTotalCount" apps/web/core/components/issues/issue-layouts/calendar/

Repository: makeplane/plane

Length of output: 582


🏁 Script executed:

# Look for load-more or pagination patterns in the calendar component
rg -n "load.*more|loadMore|pagination|offset|page" apps/web/core/components/issues/issue-layouts/calendar/roots/workspace-root.tsx -i

Repository: makeplane/plane

Length of output: 511


🏁 Script executed:

# Check how noDateTotalCount and noDateIssueIds are used in the render/display
sed -n '1,300p' apps/web/core/components/issues/issue-layouts/calendar/roots/workspace-root.tsx | grep -n "noDate\|NoDate" -C 2

Repository: makeplane/plane

Length of output: 1373


🏁 Script executed:

# Look at the return/render section to see how no-date issues are displayed
tail -100 apps/web/core/components/issues/issue-layouts/calendar/roots/workspace-root.tsx

Repository: makeplane/plane

Length of output: 3439


🏁 Script executed:

# Check if loadMoreIssues is called for no-date issues anywhere
rg -n "loadMoreIssues|noDateIssueIds" apps/web/core/components/issues/issue-layouts/calendar/roots/workspace-root.tsx -B 2 -A 2

Repository: makeplane/plane

Length of output: 751


🏁 Script executed:

# Check the CalendarChart component to see how it handles noDateIssueIds
rg -n "noDateIssueIds\|noDateIssueCount" apps/web/core/components/issues/issue-layouts/calendar -B 3 -A 3

Repository: makeplane/plane

Length of output: 41


🏁 Script executed:

# Verify if response.total_count is available and could be used
sed -n '114,165p' apps/web/core/components/issues/issue-layouts/calendar/roots/workspace-root.tsx

Repository: makeplane/plane

Length of output: 2180


🏁 Script executed:

# Check how total_count is used in other parts of the codebase for pagination
rg -n "response\.total_count\|response\.total_pages\|response\.next_page_results" apps/web/core/components/issues --max-count=10

Repository: makeplane/plane

Length of output: 41


🏁 Script executed:

# Check the CalendarChart component signature to understand how noDateIssueCount is used
rg -n "interface.*CalendarChart|type.*CalendarChart" -A 30 apps/web/core/components/issues/issue-layouts/calendar/ | head -100

Repository: makeplane/plane

Length of output: 41


🏁 Script executed:

# Look for how pagination works for the main calendar issues
sed -n '70,110p' apps/web/core/components/issues/issue-layouts/calendar/roots/workspace-root.tsx

Repository: makeplane/plane

Length of output: 1902


Use API's total_count field and implement pagination for no-date issues.

The no-date list is capped at perPageCount: 50 with setNoDateTotalCount(issueIds.length), so additional issues are hidden and the count is misreported when there are more than 50 no-date issues. The TIssuesResponse already includes total_count, next_page_results, and pagination cursors. Implement load-more logic similar to the main calendar (using loadMoreIssues pattern) to fetch and display all no-date issues, or remove the perPageCount limit if pagination isn't needed.

🤖 Prompt for AI Agents
In
`@apps/web/core/components/issues/issue-layouts/calendar/roots/workspace-root.tsx`
around lines 103 - 165, The no-date fetch (fetchNoDateIssues) currently uses
perPageCount: 50 and sets setNoDateTotalCount(issueIds.length), which caps and
misreports totals; change it to read TIssuesResponse.total_count for total count
and implement pagination/load-more using the same pattern as the main calendar
(use workspaceService.getViewIssues response.next_page_results / cursors and a
loadMoreNoDateIssues handler or reuse loadMoreIssues) to request additional
pages instead of relying on a single 50-item request; accumulate results by
appending new issues to the existing no-date IDs (setNoDateIssueIds) and calling
addIssuesToMap for each page, and only fall back to client-side filtering of
issue.target_date while preserving the API pagination cursors rather than
truncating by array.length.

Comment on lines +190 to +203
const handleDeleteIssue = async () => {
const draggedIssue = getIssueById(draggedIssueId ?? "");

if (!draggedIssueId || !draggedIssue) return;

try {
await removeIssue(draggedIssue.project_id, draggedIssueId);
setDeleteIssueModal(false);
setDraggedIssueId(undefined);
} catch (_error) {
setDeleteIssueModal(false);
setDraggedIssueId(undefined);
}
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don’t swallow delete failures—let the modal surface errors.

DeleteIssueModal relies on a rejected promise to show error toasts; the current try/catch always resolves.

🐛 Propagate delete errors
   const handleDeleteIssue = async () => {
     const draggedIssue = getIssueById(draggedIssueId ?? "");

     if (!draggedIssueId || !draggedIssue) return;

     try {
       await removeIssue(draggedIssue.project_id, draggedIssueId);
       setDeleteIssueModal(false);
       setDraggedIssueId(undefined);
-    } catch (_error) {
+    } catch (error) {
       setDeleteIssueModal(false);
       setDraggedIssueId(undefined);
+      throw error;
     }
   };
🤖 Prompt for AI Agents
In
`@apps/web/core/components/issues/issue-layouts/kanban/roots/workspace-root.tsx`
around lines 190 - 203, The current handleDeleteIssue swallows errors by
catching all exceptions and always resolving, preventing DeleteIssueModal from
receiving rejections and showing error toasts; update handleDeleteIssue so that
you await removeIssue(draggedIssue.project_id, draggedIssueId) and only call
setDeleteIssueModal(false) and setDraggedIssueId(undefined) on success, but do
not swallow failures—either remove the try/catch entirely or rethrow the caught
error in the catch block (keep references to handleDeleteIssue, removeIssue,
setDeleteIssueModal, setDraggedIssueId, and DeleteIssueModal to locate the
change).

Comment on lines +205 to +218
const handleCollapsedGroups = useCallback(
(toggle: "group_by" | "sub_group_by", value: string) => {
if (workspaceSlug) {
let collapsedGroups = issuesFilter?.issueFilters?.kanbanFilters?.[toggle] || [];
if (collapsedGroups.includes(value)) {
collapsedGroups = collapsedGroups.filter((_value) => _value != value);
} else {
collapsedGroups.push(value);
}
// projectId is not used for workspace-level filters
void updateFilters("", EIssueFilterType.KANBAN_FILTERS, {
[toggle]: collapsedGroups,
});
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid mutating the collapsedGroups array in place.

push() mutates the store‑backed array before updateFilters runs; use a new array to keep updates atomic.

✅ Safer array update
-        let collapsedGroups = issuesFilter?.issueFilters?.kanbanFilters?.[toggle] || [];
-        if (collapsedGroups.includes(value)) {
-          collapsedGroups = collapsedGroups.filter((_value) => _value != value);
-        } else {
-          collapsedGroups.push(value);
-        }
+        const current = issuesFilter?.issueFilters?.kanbanFilters?.[toggle] ?? [];
+        const collapsedGroups = current.includes(value)
+          ? current.filter((_value) => _value !== value)
+          : [...current, value];
         // projectId is not used for workspace-level filters
         void updateFilters("", EIssueFilterType.KANBAN_FILTERS, {
           [toggle]: collapsedGroups,
         });
📝 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
const handleCollapsedGroups = useCallback(
(toggle: "group_by" | "sub_group_by", value: string) => {
if (workspaceSlug) {
let collapsedGroups = issuesFilter?.issueFilters?.kanbanFilters?.[toggle] || [];
if (collapsedGroups.includes(value)) {
collapsedGroups = collapsedGroups.filter((_value) => _value != value);
} else {
collapsedGroups.push(value);
}
// projectId is not used for workspace-level filters
void updateFilters("", EIssueFilterType.KANBAN_FILTERS, {
[toggle]: collapsedGroups,
});
}
const handleCollapsedGroups = useCallback(
(toggle: "group_by" | "sub_group_by", value: string) => {
if (workspaceSlug) {
const current = issuesFilter?.issueFilters?.kanbanFilters?.[toggle] ?? [];
const collapsedGroups = current.includes(value)
? current.filter((_value) => _value !== value)
: [...current, value];
// projectId is not used for workspace-level filters
void updateFilters("", EIssueFilterType.KANBAN_FILTERS, {
[toggle]: collapsedGroups,
});
}
🤖 Prompt for AI Agents
In
`@apps/web/core/components/issues/issue-layouts/kanban/roots/workspace-root.tsx`
around lines 205 - 218, handleCollapsedGroups currently mutates the store-backed
collapsedGroups array with push(), which can cause non-atomic updates; instead
create a new array before calling updateFilters: read the existing array from
issuesFilter?.issueFilters?.kanbanFilters?.[toggle] into collapsedGroups, then
if value is included produce a new array using filter to remove it, otherwise
produce a new array by concatenating the value (e.g., [...collapsedGroups,
value]); pass that new array to updateFilters (function updateFilters) so the
original store array is never mutated in place.

Comment on lines +69 to +87
// Map state_detail.group from prePopulatedData to an actual state_id for the selected project
const resolvedPrePopulatedData = useMemo(() => {
if (!selectedProjectId || !prePopulatedData) return prePopulatedData;

// Check if prePopulatedData has state_detail.group that needs to be resolved
const stateGroup = (prePopulatedData as Record<string, unknown>)["state_detail.group"] as string | undefined;
if (!stateGroup) return prePopulatedData;

// Find a state in the selected project that belongs to this state group
const projectStates = getProjectStates(selectedProjectId);
const targetState = findStateByGroup(projectStates, stateGroup);

if (targetState) {
// Return prePopulatedData with state_id set and state_detail.group removed
const { "state_detail.group": _removed, ...rest } = prePopulatedData as Record<string, unknown>;
return { ...rest, state_id: targetState.id } as Partial<TIssue>;
}

return prePopulatedData;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Strip state_detail.group even if no state match is found.
If a project lacks a state in that group, the payload keeps "state_detail.group", which may be rejected by the API. Consider always removing that key and only adding state_id when resolved.

🔧 Suggested fix
-    if (targetState) {
-      // Return prePopulatedData with state_id set and state_detail.group removed
-      const { "state_detail.group": _removed, ...rest } = prePopulatedData as Record<string, unknown>;
-      return { ...rest, state_id: targetState.id } as Partial<TIssue>;
-    }
-
-    return prePopulatedData;
+    // Always remove state_detail.group once it's been consumed
+    const { "state_detail.group": _removed, ...rest } = prePopulatedData as Record<string, unknown>;
+    if (targetState) {
+      return { ...rest, state_id: targetState.id } as Partial<TIssue>;
+    }
+
+    return rest as Partial<TIssue>;
📝 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
// Map state_detail.group from prePopulatedData to an actual state_id for the selected project
const resolvedPrePopulatedData = useMemo(() => {
if (!selectedProjectId || !prePopulatedData) return prePopulatedData;
// Check if prePopulatedData has state_detail.group that needs to be resolved
const stateGroup = (prePopulatedData as Record<string, unknown>)["state_detail.group"] as string | undefined;
if (!stateGroup) return prePopulatedData;
// Find a state in the selected project that belongs to this state group
const projectStates = getProjectStates(selectedProjectId);
const targetState = findStateByGroup(projectStates, stateGroup);
if (targetState) {
// Return prePopulatedData with state_id set and state_detail.group removed
const { "state_detail.group": _removed, ...rest } = prePopulatedData as Record<string, unknown>;
return { ...rest, state_id: targetState.id } as Partial<TIssue>;
}
return prePopulatedData;
// Map state_detail.group from prePopulatedData to an actual state_id for the selected project
const resolvedPrePopulatedData = useMemo(() => {
if (!selectedProjectId || !prePopulatedData) return prePopulatedData;
// Check if prePopulatedData has state_detail.group that needs to be resolved
const stateGroup = (prePopulatedData as Record<string, unknown>)["state_detail.group"] as string | undefined;
if (!stateGroup) return prePopulatedData;
// Find a state in the selected project that belongs to this state group
const projectStates = getProjectStates(selectedProjectId);
const targetState = findStateByGroup(projectStates, stateGroup);
// Always remove state_detail.group once it's been consumed
const { "state_detail.group": _removed, ...rest } = prePopulatedData as Record<string, unknown>;
if (targetState) {
return { ...rest, state_id: targetState.id } as Partial<TIssue>;
}
return rest as Partial<TIssue>;
🤖 Prompt for AI Agents
In `@apps/web/core/components/issues/issue-layouts/quick-add/workspace-root.tsx`
around lines 69 - 87, The logic in resolvedPrePopulatedData (useMemo) currently
retains "state_detail.group" when no matching state is found; update it so you
always remove the "state_detail.group" key from prePopulatedData and only add
state_id when findStateByGroup(projectStates, stateGroup) returns a targetState;
locate the resolution in resolvedPrePopulatedData (references:
selectedProjectId, prePopulatedData, getProjectStates, findStateByGroup, TIssue)
and return the spread rest without the "state_detail.group" field in both
branches, conditionally merging state_id = targetState.id only when targetState
exists.

Comment on lines 563 to 597
// update updatedIssue values based on the source and destination groupIds
if (source.groupId && destination.groupId && source.groupId !== destination.groupId && groupBy) {
const groupKey = ISSUE_FILTER_DEFAULT_DATA[groupBy];
let groupValue: any = clone(sourceIssue[groupKey]);

// If groupValues is an array, remove source groupId and add destination groupId
if (Array.isArray(groupValue)) {
pull(groupValue, source.groupId);
if (destination.groupId !== "None") groupValue = uniq(concat(groupValue, [destination.groupId]));
} // else just update the groupValue based on destination groupId
else {
groupValue = destination.groupId === "None" ? null : destination.groupId;
// Special handling for state_detail.group - need to map to actual state_id
if (groupBy === "state_detail.group") {
const { getProjectStates } = store.state;
const projectStates = sourceIssue.project_id ? getProjectStates(sourceIssue.project_id) : undefined;
const targetState = findStateByGroup(projectStates, destination.groupId);

if (targetState) {
updatedIssue = { ...updatedIssue, state_id: targetState.id };
issueUpdates["state_id"] = {
ADD: [targetState.id],
REMOVE: sourceIssue.state_id ? [sourceIssue.state_id] : [],
};
} else if (projectStates) {
// No matching state found in project - cannot complete drag
throw new Error(`No state found for group "${destination.groupId}" in project`);
}
} else {
const groupKey = ISSUE_FILTER_DEFAULT_DATA[groupBy];
let groupValue: any = clone(sourceIssue[groupKey]);

// If groupValues is an array, remove source groupId and add destination groupId
if (Array.isArray(groupValue)) {
pull(groupValue, source.groupId);
if (destination.groupId !== "None") groupValue = uniq(concat(groupValue, [destination.groupId]));
} // else just update the groupValue based on destination groupId
else {
groupValue = destination.groupId === "None" ? null : destination.groupId;
}

// keep track of updates on what was added and what was removed
issueUpdates[groupKey] = { ADD: getGroupId(destination.groupId), REMOVE: getGroupId(source.groupId) };
updatedIssue = { ...updatedIssue, [groupKey]: groupValue };
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Find the findStateByGroup function definition
rg "findStateByGroup" --type ts --type tsx -B 2 -A 10

Repository: makeplane/plane

Length of output: 86


🏁 Script executed:

# Look at the broader context of the file to understand error handling patterns
fd "utils.tsx" | grep -E "issue-layouts"

Repository: makeplane/plane

Length of output: 226


🏁 Script executed:

# Check how sourceIssue.project_id is validated before reaching this code
rg "sourceIssue.project_id" --type ts --type tsx -B 3 -A 3

Repository: makeplane/plane

Length of output: 86


🏁 Script executed:

# Search for store.state.getProjectStates usage elsewhere
rg "getProjectStates" --type ts --type tsx -B 2 -A 5

Repository: makeplane/plane

Length of output: 86


🏁 Script executed:

# Search for findStateByGroup function with correct file type
rg "findStateByGroup" -t typescript -B 2 -A 10

Repository: makeplane/plane

Length of output: 93


🏁 Script executed:

# Read the specific file to see the exact code and context
cat -n apps/web/core/components/issues/issue-layouts/utils.tsx | sed -n '550,610p'

Repository: makeplane/plane

Length of output: 3256


🏁 Script executed:

# Search for getProjectStates calls to understand usage patterns
rg "getProjectStates" -t typescript -B 2 -A 5

Repository: makeplane/plane

Length of output: 93


🏁 Script executed:

# Check how this function is called to see if project_id is guaranteed
rg "handleDragEnd|updateDraggedIssue" -t typescript -B 3 -A 3

Repository: makeplane/plane

Length of output: 93


🏁 Script executed:

# Search for findStateByGroup definition without type flag
rg "function findStateByGroup|const findStateByGroup|export.*findStateByGroup" -B 2 -A 8

Repository: makeplane/plane

Length of output: 1056


🏁 Script executed:

# Look for where sourceIssue comes from and if project_id is validated
rg "sourceIssue\s*=" apps/web/core/components/issues/issue-layouts/utils.tsx -B 5 -A 2

Repository: makeplane/plane

Length of output: 311


🏁 Script executed:

# Check the function signature that contains this code
head -n 570 apps/web/core/components/issues/issue-layouts/utils.tsx | tail -n 100 | grep -E "^(const|function|export)" | tail -1

Repository: makeplane/plane

Length of output: 101


🏁 Script executed:

# Search for findStateByGroup in the entire file
grep -n "findStateByGroup" apps/web/core/components/issues/issue-layouts/utils.tsx

Repository: makeplane/plane

Length of output: 266


Guard against missing project states during state-group DnD.
When sourceIssue.project_id is falsy, projectStates becomes undefined, causing findStateByGroup to return undefined and the drop to proceed silently without updating state_id. The issue will revert to its original state after refresh. Add an explicit check and error when projectStates is undefined, or block the drop for state-group operations missing project context.

🤖 Prompt for AI Agents
In `@apps/web/core/components/issues/issue-layouts/utils.tsx` around lines 563 -
597, When handling groupBy === "state_detail.group", guard against a missing
project state list by checking the result of
getProjectStates(sourceIssue.project_id) and if projectStates is undefined (i.e.
sourceIssue.project_id is falsy or no states returned) throw an explicit Error
(or otherwise block the drop) before calling findStateByGroup; update the logic
around getProjectStates, findStateByGroup, updatedIssue and issueUpdates to
ensure you only set state_id and issueUpdates when a valid targetState is found
and otherwise reject the operation with a clear error message.

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]: Calendar/Kabana View for Individual User across the Projects

2 participants