Implementation of the feature #6642 Calendar/Kanban View for Individual User across the Projects#8588
Conversation
… Individual User across the Projects
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds workspace-level calendar and kanban roots, grouped issue listing with validated grouping/pagination, workspace quick-add with state-group resolution, layout-aware filter/store adjustments, new helper utilities and store APIs, UI/type refinements across calendar/kanban/quick-add components, and backend grouping/filter enhancements. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant WorkspaceCalendarRoot
participant FilterStore
participant IssueStore
participant WorkspaceService
participant CalendarChart
User->>WorkspaceCalendarRoot: Mount (globalViewId)
WorkspaceCalendarRoot->>FilterStore: getAppliedFilters / layout & date range
WorkspaceCalendarRoot->>IssueStore: fetchIssues(date range, viewId)
IssueStore->>WorkspaceService: GET /view/issues (date range)
WorkspaceService-->>IssueStore: TIssuesResponse
IssueStore-->>WorkspaceCalendarRoot: Issues loaded
WorkspaceCalendarRoot->>WorkspaceService: GET /view/issues (target_date__isnull)
WorkspaceService-->>WorkspaceCalendarRoot: No-date issues
WorkspaceCalendarRoot->>IssueStore: addIssuesToMap(no-date issues)
WorkspaceCalendarRoot->>CalendarChart: render(issues, handlers)
User->>CalendarChart: Drag / Toggle No-Date
CalendarChart->>WorkspaceCalendarRoot: handleDragAndDrop / toggle
WorkspaceCalendarRoot->>IssueStore: update issue / call actions
WorkspaceCalendarRoot->>FilterStore: updateFilters (collapse/group)
sequenceDiagram
participant User
participant WorkspaceKanBanRoot
participant FilterStore
participant IssueStore
participant WorkspaceService
participant KanBan
User->>WorkspaceKanBanRoot: Mount (globalViewId)
WorkspaceKanBanRoot->>FilterStore: get display_filters (group_by)
WorkspaceKanBanRoot->>IssueStore: fetchIssues/grouped (viewId)
IssueStore->>WorkspaceService: GET /view/issues (grouping)
WorkspaceService-->>IssueStore: TIssuesResponse (grouped)
WorkspaceKanBanRoot->>KanBan: render(groups, permissions)
User->>KanBan: Drag to delete
KanBan->>WorkspaceKanBanRoot: handleDragAndDrop
WorkspaceKanBanRoot->>IssueStore: archiveIssue(projectId, issueId)
IssueStore-->>WorkspaceKanBanRoot: success
KanBan->>FilterStore: updateFilters (collapse state)
sequenceDiagram
participant User
participant WorkspaceQuickAddRoot
participant ProjectSelector
participant StateResolver
participant IssueStore
participant WorkspaceService
User->>WorkspaceQuickAddRoot: Open quick-add
WorkspaceQuickAddRoot->>ProjectSelector: select project (auto if none)
WorkspaceQuickAddRoot->>StateResolver: resolve state_detail.group -> state_id
StateResolver->>WorkspaceService: getProjectStates(projectId)
WorkspaceService-->>StateResolver: project states
StateResolver-->>WorkspaceQuickAddRoot: resolved state_id
User->>WorkspaceQuickAddRoot: Submit form
WorkspaceQuickAddRoot->>IssueStore: quickAddIssue(projectId, payload)
IssueStore->>IssueStore: add temp issue to map
IssueStore->>WorkspaceService: POST /issues (create)
WorkspaceService-->>IssueStore: created issue
IssueStore->>WorkspaceQuickAddRoot: replace temp with real issue
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
No actionable comments were generated in the recent review. 🎉 🧹 Recent nitpick comments
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.
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.
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.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@apps/web/core/store/issue/workspace/filter.store.ts`:
- Around line 216-217: The fire-and-forget calls using void
this.rootIssueStore.workspaceIssues.fetchIssuesWithExistingPagination(workspaceSlug,
viewId, "mutation") (and the same pattern elsewhere) can reject and cause
unhandled promise rejections; update each such call (including the occurrences
around lines with workspaceSlug/viewId and the other calls at the noted
locations) to append a .catch(...) handler that logs or swallows the error via
your logger (e.g., processLogger or root logger) so rejections are handled;
ensure you reference the same function name fetchIssuesWithExistingPagination
and owner rootIssueStore.workspaceIssues when adding the .catch for all
fire-and-forget invocations.
🧹 Nitpick comments (2)
apps/web/core/store/issue/workspace/filter.store.ts (2)
186-195: Duplicated kanban/calendar default logic betweenfetchFiltersandupdateFilters.The kanban
group_bydefaulting (lines 186-190 ≈ 257-264) and calendar defaults (lines 193-195 ≈ 272-275) are repeated verbatim. Consider extracting a shared helper (e.g.,applyLayoutDefaults(displayFilters)) to keep the two paths in sync and reduce the chance of future divergence.Also applies to: 255-275
296-310: Emptyifbranch is a code smell — prefer inverting the condition.The calendar branch (lines 296-298) is intentionally empty. Inverting the conditions or using an early
break/guard avoids the empty block and makes intent clearer.Suggested refactor
- if (updatedDisplayFilters.layout === "calendar") { - // Calendar layout needs date-range parameters that only the component can provide - // Don't fetch here - let the calendar component handle it - } else if (updatedDisplayFilters.layout) { + if (updatedDisplayFilters.layout && updatedDisplayFilters.layout !== "calendar") { // Layout is changing to kanban or spreadsheet - fetch with correct canGroup const needsGrouping = _filters.displayFilters.layout === "kanban"; - void this.rootIssueStore.workspaceIssues.fetchIssues( + this.rootIssueStore.workspaceIssues.fetchIssues( workspaceSlug, viewId, "init-loader", { canGroup: needsGrouping, perPageCount: needsGrouping ? 30 : 100 } - ); - } else { - void this.rootIssueStore.workspaceIssues.fetchIssuesWithExistingPagination(workspaceSlug, viewId, "mutation"); + ).catch((err) => console.error("Failed to fetch issues after layout change", err)); + } else if (!updatedDisplayFilters.layout) { + this.rootIssueStore.workspaceIssues.fetchIssuesWithExistingPagination(workspaceSlug, viewId, "mutation") + .catch((err) => console.error("Failed to fetch issues after filter change", err)); }
…re-and-forget calls Replace void-prefixed fire-and-forget calls with .catch() handlers to prevent unhandled promise rejections from background fetches. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…youtDefaults helper Deduplicate the kanban group_by defaulting and calendar config defaulting logic that was repeated in both fetchFilters and updateFilters. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…endar root Change import from non-existent @/plane-web/services to @/services/workspace.service which is where WorkspaceService is defined. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/web/core/store/issue/workspace/filter.store.ts (1)
336-339: 🛠️ Refactor suggestion | 🟠 MajorUse the
STATIC_VIEW_TYPESconstant instead of the hardcoded array.
STATIC_VIEW_TYPESis imported at line 24 and already used at lines 164 and 198 in this file. The hardcoded["all-issues", "assigned", "created", "subscribed"]at line 336 duplicates the constant definition and risks drifting out of sync if it changes.Suggested fix
- if (["all-issues", "assigned", "created", "subscribed"].includes(viewId)) + if (STATIC_VIEW_TYPES.includes(viewId))
🤖 Fix all issues with AI agents
In `@apps/web/core/store/issue/workspace/filter.store.ts`:
- Around line 297-302: Summary: avoid clearing issues when the incoming layout
is identical to the current one to prevent unnecessary loader flashes. Fix: in
the branch that currently checks updatedDisplayFilters.layout, compare the new
layout value against the existing layout (e.g., this.displayFilters.layout) and
only call this.rootIssueStore.workspaceIssues.clearIssueIds() when they differ;
keep the existing behavior of clearing BEFORE applying the layout update (so
IssueLayoutHOC sees undefined issueCount) but skip the clear if no actual
change.
🧹 Nitpick comments (4)
apps/web/core/components/issues/issue-layouts/calendar/roots/workspace-root.tsx (2)
25-31: UnusedisDefaultViewprop.
isDefaultViewis declared inPropsbut never referenced in the component body (onlyglobalViewIdis destructured on line 31). Either remove it from the type or use it.Proposed fix
type Props = { - isDefaultView: boolean; globalViewId: string; };
192-224:handleDragAndDropis not memoized — new reference every render.This async handler is passed as a prop to
CalendarChart. WithoutuseCallback, a new closure is created on every render, which can trigger unnecessary re-renders of the chart (and its children). The other callbacks (loadMoreIssues,getPaginationData,getGroupIssueCount,canEditProperties) are already wrapped inuseCallback.Wrap in useCallback
- const handleDragAndDrop = async ( + const handleDragAndDrop = useCallback(async ( issueId: string | undefined, issueProjectId: string | undefined, sourceDate: string | undefined, destinationDate: string | undefined ) => { if (!issueId || !destinationDate || !sourceDate || !issueProjectId) return; if (!canEditPropertiesBasedOnProject(issueProjectId)) { setToast({ title: "Permission denied", type: TOAST_TYPE.ERROR, message: "You don't have permission to edit this issue", }); return; } await handleDragDrop( issueId, sourceDate, destinationDate, workspaceSlug?.toString(), issueProjectId, updateIssue ).catch((err: { detail?: string }) => { setToast({ title: "Error!", type: TOAST_TYPE.ERROR, message: err?.detail ?? "Failed to perform this action", }); }); - }; + }, [canEditPropertiesBasedOnProject, workspaceSlug, updateIssue]);apps/web/core/store/issue/workspace/filter.store.ts (2)
86-106: Consider usingEIssueLayoutTypesenum values instead of string literals.Lines 92 and 103 use
"kanban"and"calendar"string literals, while line 127 usesEIssueLayoutTypes.SPREADSHEET. Using the enum consistently (e.g.,EIssueLayoutTypes.KANBAN) prevents silent breakage if the enum values ever change, and keeps the file self-consistent.The same applies to other string literal layout comparisons on lines 271, 290, 317, and 319.
319-324: Extract magic numbers into named constants.
30and100forperPageCountare used without explanation. Named constants clarify intent and prevent inconsistencies if the same values appear elsewhere.Example
+const KANBAN_PER_PAGE_COUNT = 30; +const LIST_PER_PAGE_COUNT = 100; // ... - { canGroup: needsGrouping, perPageCount: needsGrouping ? 30 : 100 } + { canGroup: needsGrouping, perPageCount: needsGrouping ? KANBAN_PER_PAGE_COUNT : LIST_PER_PAGE_COUNT }
… flash Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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
Enhancements