-
Notifications
You must be signed in to change notification settings - Fork 3.5k
Implementation of the feature #6642 Calendar/Kanban View for Individual User across the Projects #8588
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: preview
Are you sure you want to change the base?
Implementation of the feature #6642 Calendar/Kanban View for Individual User across the Projects #8588
Conversation
… Individual User across the Projects
📝 WalkthroughWalkthroughAdds workspace-level calendar and kanban UIs and supporting APIs: grouped pagination for workspace issue lists, workspace roots for calendar/kanban/quick-add, filter/type consolidations, drag-drop state-group mapping, store/hook extensions, and new constants for workspace kanban grouping and DRAG_ALLOWED_GROUPS. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant WorkspaceCalendarRoot
participant IssueStore
participant WorkspaceService
participant CalendarChart
User->>WorkspaceCalendarRoot: open workspace calendar
WorkspaceCalendarRoot->>IssueStore: fetch grouped issues (by date)
IssueStore->>WorkspaceService: GET /view/issues (group_by=target_date)
WorkspaceService-->>IssueStore: grouped issues response
WorkspaceCalendarRoot->>IssueStore: fetch no-date issues (target_date__isnull=true)
WorkspaceService-->>IssueStore: no-date issues
WorkspaceCalendarRoot->>CalendarChart: render grouped + no-date sections
User->>CalendarChart: drag issue to date
CalendarChart->>WorkspaceCalendarRoot: handleDragDrop(issue, date)
WorkspaceCalendarRoot->>IssueStore: update issue.target_date
IssueStore->>WorkspaceService: PATCH issue
WorkspaceService-->>IssueStore: confirmation
IssueStore-->>CalendarChart: UI updates
sequenceDiagram
participant User
participant WorkspaceKanBanRoot
participant IssueStore
participant WorkspaceService
participant KanBanComponent
participant DeleteModal
User->>WorkspaceKanBanRoot: open workspace kanban
WorkspaceKanBanRoot->>IssueStore: fetch grouped issues (group_by=state_detail.group)
IssueStore->>WorkspaceService: GET /view/issues (group_by=state_detail.group)
WorkspaceService-->>IssueStore: grouped issues
WorkspaceKanBanRoot->>KanBanComponent: render groups
User->>KanBanComponent: drag issue to delete zone
KanBanComponent->>WorkspaceKanBanRoot: onDrop(issue)
WorkspaceKanBanRoot->>DeleteModal: show confirm
User->>DeleteModal: confirm
WorkspaceKanBanRoot->>IssueStore: remove issue
IssueStore->>WorkspaceService: DELETE issue
WorkspaceService-->>IssueStore: confirmation
IssueStore-->>KanBanComponent: UI updates
sequenceDiagram
participant User
participant WorkspaceQuickAddIssueRoot
participant WorkspaceService
participant IssueStore
participant Toast
User->>WorkspaceQuickAddIssueRoot: open quick-add
WorkspaceQuickAddIssueRoot->>WorkspaceService: getProjectStates(projectId)
WorkspaceService-->>WorkspaceQuickAddIssueRoot: states
WorkspaceQuickAddIssueRoot->>WorkspaceQuickAddIssueRoot: resolve state via findStateByGroup
User->>WorkspaceQuickAddIssueRoot: submit form
WorkspaceQuickAddIssueRoot->>WorkspaceService: createIssue(payload)
WorkspaceService-->>WorkspaceQuickAddIssueRoot: created issue
WorkspaceQuickAddIssueRoot->>IssueStore: addIssuesToMap(issue)
WorkspaceQuickAddIssueRoot->>Toast: show success
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this 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.
fetchIssuesnow accepts an explicitviewId, butfetchNextIssuesstill uses the routerglobalViewId. If callers pass a viewId (workspace-level views), pagination can no-op or page the wrong list. Consider mirroring the sameeffectiveViewIdlogic infetchNextIssues.💡 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 inworkspace-root.tsxforgetViewIssuesresponse.The
getViewIssuesmethod now returnsPromise<TIssuesResponse | undefined>for canceled requests, butworkspace-root.tsxline 133 declares the response asconst response: TIssuesResponse(missing| undefined). This will cause type errors in strict mode. Update the type toTIssuesResponse | undefinedor remove the explicit type annotation to infer it correctly. The runtime guardif (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.currentin useEffect dependency array.
dayTileRef?.currentin 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
handleDragAndDropandissuesin the dependency array since they're used inside the effect, or wrap them inuseCallback/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: UnusedworkspaceSlugprop inGlobalViewLayoutSelection.The
workspaceSlugproperty is defined inTLayoutSelectionPropsbut 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 allowedgroup_byandsub_group_byfield values.The code correctly prevents
group_byandsub_group_byfrom 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_valuesreturns 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: ComputecanCreateIssuesonce 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
apps/web/core/components/issues/issue-layouts/calendar/roots/workspace-root.tsx
Show resolved
Hide resolved
apps/web/core/components/issues/issue-layouts/kanban/roots/workspace-root.tsx
Show resolved
Hide resolved
apps/web/core/components/issues/issue-layouts/kanban/roots/workspace-root.tsx
Show resolved
Hide resolved
apps/web/core/components/issues/issue-layouts/quick-add/workspace-root.tsx
Outdated
Show resolved
Hide resolved
…ent pagination Mirror the effectiveViewId logic from fetchIssues in fetchNextIssues so that workspace-level views using an explicit viewId paginate correctly. Also fix dependency array referencing fetchIssues instead of fetchNextIssues.
…Issues response Let TypeScript infer the return type (TIssuesResponse | undefined) instead of explicitly annotating as TIssuesResponse, which is incorrect since getViewIssues can return undefined for canceled requests.
…nban When group_by is forced to state_detail.group for workspace kanban views, sub_group_by could still equal it, bypassing the earlier duplicate guard. Added a post-normalization check to nullify sub_group_by in that case.
Remove dayTileRef.current from dependency array (ref mutations don't trigger re-renders) and add handleDragAndDrop and issues to prevent stale closures in the drop handler.
The prop was defined in the type and passed by the caller but never used in GlobalViewLayoutSelection. Removed from both the type and the call site.
Reject invalid field names with a 400 error instead of silently returning empty groups. Validates against the set of fields supported by issue_group_values.
Replace useCallback returning a function with useMemo returning a boolean to avoid duplicate permission scans across joinedProjectIds on every render. Applied to both kanban and calendar workspace roots.
Use response.total_count instead of issueIds.length for accurate count when there are more no-date issues than the page size. Increased perPageCount from 50 to 500 to cover most real-world cases without needing full cursor-based pagination.
Use .finally() for cleanup instead of try/catch that swallowed errors. This allows DeleteIssueModal to surface error toasts on failed deletes.
Use spread to create a new array instead of push() which mutates the MobX store array before updateFilters runs.
Remove the synthetic state_detail.group key regardless of whether a matching state is found, preventing the API from receiving an unrecognized field.
Throw early when sourceIssue.project_id is falsy instead of silently skipping the state_id update. Also simplify the else branch to always throw when no matching state is found, since projectStates is now guaranteed to be defined.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@apps/api/plane/app/views/view/base.py`:
- Around line 278-281: The code always calls issue_queryset_grouper even when
grouping isn't requested, causing expensive subquery annotations (e.g.,
assignee_ids, label_ids, module_ids) to be added unnecessarily; modify
issue_queryset_grouper to return the original queryset immediately when both
group_by and sub_group_by are falsy (add an early return at the top of
issue_queryset_grouper) so no annotations are applied when grouping is not used,
and keep the existing behavior for the rest of the function.
🧹 Nitpick comments (1)
apps/web/core/store/issue/workspace/filter.store.ts (1)
210-220: Consider logging typed errors withconsole.error.
Normalizing unknown error values improves log quality and consistency.♻️ Suggested change
} catch (error) { - console.log("error while updating rich filters", error); - throw error; + const err = error instanceof Error ? error : new Error(String(error)); + console.error("error while updating rich filters", err); + throw err; }As per coding guidelines: Use try-catch with proper error types and log errors appropriately.
| # Apply grouper to issue queryset | ||
| issue_queryset = issue_queryset_grouper( | ||
| queryset=issue_queryset, group_by=group_by, sub_group_by=sub_group_by | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
rg -n "def issue_queryset_grouper" -A 30 apps/api/plane/app/views/view/base.pyRepository: makeplane/plane
Length of output: 41
🏁 Script executed:
rg -n "def issue_queryset_grouper" -A 30Repository: makeplane/plane
Length of output: 4567
🏁 Script executed:
rg -n "def issue_queryset_grouper" -A 100 apps/api/plane/utils/grouper.py | head -80Repository: makeplane/plane
Length of output: 2965
🏁 Script executed:
sed -n '270,290p' apps/api/plane/app/views/view/base.pyRepository: makeplane/plane
Length of output: 879
Unconditional invocation of grouper applies unnecessary annotations when grouping is not used.
The issue_queryset_grouper function is called even when group_by and sub_group_by are both falsy. While the function safely skips filters in those cases (lines 41-43 in grouper.py), it still unconditionally applies all subquery annotations to the queryset (lines 81-86). When grouping is not needed, these annotations—particularly the assignee_ids, label_ids, and module_ids subqueries—are computed unnecessarily, adding overhead to the query. Consider adding an early return when neither grouping parameter is provided.
🤖 Prompt for AI Agents
In `@apps/api/plane/app/views/view/base.py` around lines 278 - 281, The code
always calls issue_queryset_grouper even when grouping isn't requested, causing
expensive subquery annotations (e.g., assignee_ids, label_ids, module_ids) to be
added unnecessarily; modify issue_queryset_grouper to return the original
queryset immediately when both group_by and sub_group_by are falsy (add an early
return at the top of issue_queryset_grouper) so no annotations are applied when
grouping is not used, and keep the existing behavior for the rest of the
function.
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
Changes Overview
Frontend (
apps/web)New Components
calendar/roots/workspace-root.tsxkanban/roots/workspace-root.tsxquick-add/workspace-root.tsxModified Components
calendar/calendar.tsxroots/all-issue-layout-root.tsxutils.tsxfindStateByGroup()utility for cross-project state mapping, enhanced drag-drop handling forstate_detail.groupissue-layout-HOC.tsxStore Changes
workspace/filter.store.tsstate_detail.groupas default group_by for Kanban, layout switch handlingworkspace/issue.store.tshelpers/base-issues.store.tsaddIssue()method exposure for issue map updatesHooks & Services
use-issues.tsaddIssuesToMapfunction for adding fetched issues to the storeworkspace.service.tsBackend (
apps/api)API Changes
views/view/base.pyWorkspaceViewIssuesViewSet, backward-compatible with existing clientsutils/filters/filterset.pytarget_date__isnullandstart_date__isnullfilters for "No Date" queriesPackages
Constants (
packages/constants)issue/filter.tsWORKSPACE_KANBAN_GROUP_BY_OPTIONS, updated layout configs formy_issuesfilter typeissue/common.ts"calendar"toWORKSPACE_ACTIVE_LAYOUTSTechnical Implementation Details
1. State Group to State ID Mapping
Workspace views group issues by
state_detail.group(e.g., "backlog", "started", "completed") instead of specificstate_idbecause states are project-specific. When an issue is dragged to a new state group column:findStateByGroup()utility finds a matching state in the issue's projectstate_idto the resolved state2. "No Date" Section Architecture
The Calendar view fetches issues without
target_dateseparately from date-range issues:target_date__isnull=truefilternoDateIssueIdsandnoDateTotalCountappliedFiltersKeychanges3. Grouped Pagination for Kanban
The backend
WorkspaceViewIssuesViewSetnow supports grouped pagination:group_by: Returns flat list (backward compatible)group_by: Returns grouped structure with per-group paginationGroupedOffsetPaginatorfrom project issue views4. Quick Add with Project Selection
Workspace views require project selection before creating an issue:
API Changes
WorkspaceViewIssuesViewSet
Endpoint:
GET /api/v1/workspaces/{workspace_slug}/views/issues/New Query Parameters:
group_bystate_detail.group,priority)sub_group_bytarget_date__isnullBackward Compatibility: ✅ Existing clients receive flat list when
group_byis not provided.Testing Checklist
Calendar Layout
Kanban Layout
Cross-cutting
Related Issues
Migration Notes
No database migrations required. The feature uses existing issue fields and adds optional query parameters to existing endpoints.
Rollback Plan
Summary by CodeRabbit
New Features
Bug Fixes
Improvements
✏️ Tip: You can customize this high-level summary in your review settings.