.1502411993651747:9465261c10d0fa2600e994c8bda80fc6_69f5fb46dc48703ced6cfd4e.69f5fb52dc48703ced6cfd53.69f5fb510b407201bc3fbc0e:Trae CN.T(2026/5/2 21:25:38)#8993
Conversation
|
|
📝 WalkthroughWalkthroughIntroduces end-to-end issue transfer functionality enabling users to move single or multiple issues between projects within the same workspace. Includes backend API endpoints with permission validation, complex state/label/assignee remapping logic, recursive child-issue handling, and frontend modal UI with store integration. ChangesIssue Transfer Across Projects
Sequence DiagramsequenceDiagram
participant User
participant Modal as Transfer Modal
participant Store as Issue Store
participant Service as IssueService
participant API as Backend API
participant DB as Database
User->>Modal: Select target project
Modal->>Modal: Validate issueIds & sourceProjectId
alt Single Issue
Modal->>Service: transferIssue(workspace, projectId, data)
else Multiple Issues
Modal->>Service: bulkTransferIssues(workspace, projectId, data)
end
Service->>API: POST /transfer-issue/ or /bulk-transfer-issues/
API->>API: Check user permission for target project
API->>API: Validate target project & issues exist
API->>API: Map state to target project equivalent
API->>API: Remap labels & assignees by availability
API->>DB: Update issue project_id, state, relations
API->>DB: Migrate related records (comments, reactions, etc.)
API->>DB: Recursively transfer child issues
API->>DB: Update recent visits & sequencing
API->>DB: Enqueue async activity logging
API-->>Service: Return transfer result
Service-->>Store: Provide transfer response
Store->>Store: Optimistically remove from source project
Store->>Store: Update parent stats for source project
Store->>Service: Retrieve transferred issue(s) from target
Service->>API: GET issue details from target project
API-->>Service: Return target issue data
Service-->>Store: Cache retrieved issue(s)
Store-->>Modal: Transfer complete
Modal->>User: Show success toast & close
Modal->>User: Call onSuccess callback
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes The backend logic is intricate with state/label/assignee mapping, recursive child-issue handling, and multi-entity migration. The frontend integrates across many dropdowns and introduces optimistic store updates. Multiple non-trivial edge cases and data transformation patterns warrant careful review. Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 3❌ Failed checks (3 warnings)
✅ Passed checks (2 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. Review rate limit: 7/8 reviews remaining, refill in 7 minutes and 30 seconds.Comment |
There was a problem hiding this comment.
Actionable comments posted: 10
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/api/plane/app/urls/issue.py`:
- Around line 286-295: The file uses the Django helper path() throughout to
build urlpatterns (including the routes for TransferIssueEndpoint and
BulkTransferIssuesEndpoint) but never imports it; add the missing import "from
django.urls import path" near the top of the module (alongside any existing
django.urls imports) so path is defined when Django loads urlpatterns and the
route declarations (e.g., TransferIssueEndpoint.as_view()) won't raise
NameError.
In `@apps/api/plane/utils/issue_transfer.py`:
- Around line 418-426: The recursive loop that calls transfer_issue for each
child_issue ignores and discards each child's result, so failures are not
propagated and successful responses only return the top-level issue_id; change
the logic in the transfer_issue recursion (the loop over child_issues that calls
transfer_issue with slug, source_project_id, target_project_id,
issue_id=str(child_issue.id), request, user_id) to collect each child's return
value/result, aggregate success/failure (e.g., accumulate moved_issue_ids and
bubble up any failures or partial failures), and include all descendant
issue_ids in the final response; apply the same fix to the other recursive call
site around the block for lines 465-470 so callers can remove recursively moved
descendants and any child errors cause the parent call to report failure.
- Around line 506-515: The loop in bulk_transfer_issues calls transfer_issue for
every requested id even when a parent transfer already moved its descendants;
fix by tracking moved issue ids and skipping subsequent calls for those ids.
Implement a moved_ids set in bulk_transfer_issues and, before calling
transfer_issue(issue_id=...), skip if issue_id in moved_ids; change
transfer_issue to return the list/set of moved issue IDs (or accept a moved_ids
set to populate) and update the moved_ids set with the returned/added ids after
each successful transfer so children already moved by a parent are not processed
again.
- Around line 453-463: The activity call uses an unsupported type
"issue.activity.transferred" so no activity entry is created; fix by either
switching the type passed to issue_activity.delay(...) to one of the keys
handled by the activity dispatch in issue_activities_task.py (use the exact
mapped string used there, e.g., the existing transfer-related key) or add a
handler in apps/api/plane/bgtasks/issue_activities_task.py that maps
"issue.activity.transferred" to create the transfer audit entry (reuse the same
payload fields: requested_data, actor_id, issue_id, project_id, epoch, origin,
etc.), and update/add tests to assert the transfer activity is created.
In
`@apps/web/core/components/issues/issue-layouts/quick-action-dropdowns/helper.tsx`:
- Around line 255-261: The menu item's hardcoded title in
createTransferToProjectMenuItem should be localized; replace the literal "Move
to project" with a call to the i18n function (e.g.,
t("common.actions.move_to_project")) where createTransferToProjectMenuItem
constructs the TContextMenuItem and ensure the new translation key
(common.actions.move_to_project) is added to the i18n translation files
alongside other common.actions.* keys so props.setTransferToProjectModal /
handleOptionalAction usage remains unchanged.
In `@apps/web/core/components/issues/transfer-issue-to-project-modal.tsx`:
- Line 28: The modal's search state (query) isn't reset on close; update the
modal close flow to clear it by calling setQuery("") when the modal closes (add
this in the existing handleClose function used to close the modal) or add a
useEffect that watches the isOpen prop and resets query to "" whenever isOpen
becomes false; ensure you update all close paths that call handleClose and
remove any stale query that would persist across opens (references: query,
setQuery, handleClose, isOpen).
- Around line 103-105: The close button rendering only <CloseIcon /> lacks an
accessible name; update the button that invokes handleClose to include an ARIA
label (e.g., aria-label="Close" or aria-label={`Close ${modalTitle}`} for
context) and ensure it has type="button" if missing so screen readers can
announce its purpose; locate the button element wrapping CloseIcon in the
transfer-issue-to-project modal and add the aria-label attribute (or
aria-labelledby pointing to a visible label) accordingly.
- Around line 82-90: The modal isn't closing because the bulk transfer response
lacks response.success; after awaiting transferPromise in the try block, detect
success for both single and bulk responses by checking either response.success
=== true OR (Array.isArray(response.transferred_issues) &&
response.transferred_issues.length > 0), and only then call handleClose() and
onSuccess?. Update the logic around transferPromise (the code that follows await
transferPromise) to normalize/branch on these two shapes (responses from
transferIssue vs bulkTransferIssues) so the modal closes after successful bulk
transfers.
- Around line 32-33: The modal always uses useIssues(EIssuesStoreType.PROJECT)
which causes transfers to operate on the PROJECT store only and leaves IDs stale
in CYCLE/MODULE stores; update TransferIssueToProjectModal to accept the
caller's store context and use that store for transfers: add a prop (storeType:
EIssuesStoreType) to TransferIssueToProjectModal (or alternatively accept
transferFn/bulkTransferFn props) and replace the hardcoded
useIssues(EIssuesStoreType.PROJECT) with useIssues(storeType) (or call the
injected transfer functions) so issues.transferIssue / issues.bulkTransferIssues
run on the correct store and its removeIssueFromList removes IDs from the
originating store rather than always touching the PROJECT store.
In `@apps/web/core/store/issue/issue-details/issue.store.ts`:
- Around line 215-226: The transferIssue method in issue.store.ts is passing an
object { issue_ids: string[] } to currentStore.transferIssue which expects a
single issueId string; update this to call
currentStore.bulkTransferIssues(workspaceSlug, sourceProjectId, targetProjectId,
data.issue_ids) instead (or if the intent is single-issue, extract and pass
data.issue_ids[0] to currentStore.transferIssue); locate the method around
transferIssue, currentStore (derived from
rootIssueDetailStore.rootIssueStore.projectEpics or projectIssues), and change
the call to use bulkTransferIssues with data.issue_ids.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 40c97dc1-38c9-4ed3-9ce0-1527518528f0
📒 Files selected for processing (16)
apps/api/plane/app/urls/issue.pyapps/api/plane/app/views/__init__.pyapps/api/plane/app/views/issue/base.pyapps/api/plane/utils/issue_transfer.pyapps/web/core/components/issues/issue-detail/root.tsxapps/web/core/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsxapps/web/core/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsxapps/web/core/components/issues/issue-layouts/quick-action-dropdowns/helper.tsxapps/web/core/components/issues/issue-layouts/quick-action-dropdowns/issue-detail.tsxapps/web/core/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsxapps/web/core/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsxapps/web/core/components/issues/transfer-issue-to-project-modal.tsxapps/web/core/services/issue/issue.service.tsapps/web/core/store/issue/helpers/base-issues.store.tsapps/web/core/store/issue/issue-details/issue.store.tsapps/web/core/store/issue/issue-details/root.store.ts
| path( | ||
| "workspaces/<str:slug>/projects/<uuid:project_id>/transfer-issue/", | ||
| TransferIssueEndpoint.as_view(), | ||
| name="transfer-issue", | ||
| ), | ||
| path( | ||
| "workspaces/<str:slug>/projects/<uuid:project_id>/bulk-transfer-issues/", | ||
| BulkTransferIssuesEndpoint.as_view(), | ||
| name="bulk-transfer-issues", | ||
| ), |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n "apps/api/plane/app/urls/issue.py" | head -30Repository: makeplane/plane
Length of output: 1133
🏁 Script executed:
cat -n "apps/api/plane/app/urls/issue.py" | grep -E "(^[[:space:]]*[0-9]+[[:space:]]+(from|import|path))" | head -50Repository: makeplane/plane
Length of output: 810
🏁 Script executed:
sed -n '280,300p' "apps/api/plane/app/urls/issue.py"Repository: makeplane/plane
Length of output: 614
🏁 Script executed:
sed -n '1,70p' "apps/api/plane/app/urls/issue.py" | grep -A 5 "^from django"Repository: makeplane/plane
Length of output: 41
🏁 Script executed:
rg "from django.urls|import path" "apps/api/plane/app/urls/issue.py"Repository: makeplane/plane
Length of output: 41
🏁 Script executed:
wc -l "apps/api/plane/app/urls/issue.py"Repository: makeplane/plane
Length of output: 96
🏁 Script executed:
sed -n '1,40p' "apps/api/plane/app/urls/issue.py"Repository: makeplane/plane
Length of output: 1186
🏁 Script executed:
cat "apps/api/plane/app/urls/issue.py" | head -40 | tail -10Repository: makeplane/plane
Length of output: 322
🏁 Script executed:
rg "^from django.urls import" "apps/api/plane/app/views" --max-count 3Repository: makeplane/plane
Length of output: 124
🏁 Script executed:
sed -n '1,36p' "apps/api/plane/app/urls/issue.py" | tail -5Repository: makeplane/plane
Length of output: 137
🏁 Script executed:
grep -n "path" "apps/api/plane/app/urls/issue.py" | head -3Repository: makeplane/plane
Length of output: 98
🏁 Script executed:
sed -n '1,36p' "apps/api/plane/app/urls/issue.py"Repository: makeplane/plane
Length of output: 1036
🏁 Script executed:
sed -n '1,50p' "apps/api/plane/app/urls/issue.py" | tail -20Repository: makeplane/plane
Length of output: 670
🏁 Script executed:
python3 << 'EOF'
with open("apps/api/plane/app/urls/issue.py", "r") as f:
lines = f.readlines()
# Check first 50 lines for any django.urls imports
for i, line in enumerate(lines[:50], 1):
if "django" in line.lower() or "from" in line or "import" in line:
print(f"{i}: {line.rstrip()}")
EOFRepository: makeplane/plane
Length of output: 92
🏁 Script executed:
python3 << 'EOF'
with open("apps/api/plane/app/urls/issue.py", "r") as f:
content = f.read()
# Check if "from django.urls" appears anywhere
if "from django.urls" in content:
print("Found django.urls import")
else:
print("NO django.urls import found")
# Count path usage
path_count = content.count("path(")
print(f"Uses path() {path_count} times")
# Check first 100 lines for all imports
lines = content.split('\n')[:100]
imports = [line for line in lines if line.strip().startswith(('from ', 'import '))]
print(f"\nAll imports in first 100 lines:")
for imp in imports:
print(f" {imp}")
EOFRepository: makeplane/plane
Length of output: 173
Add missing path import to enable URL routing.
This module uses path() 42 times to build urlpatterns but lacks from django.urls import path. Django will raise NameError: name 'path' is not defined when loading this URLConf module.
🐛 Minimal fix
+from django.urls import path
+
from plane.app.views import (🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/api/plane/app/urls/issue.py` around lines 286 - 295, The file uses the
Django helper path() throughout to build urlpatterns (including the routes for
TransferIssueEndpoint and BulkTransferIssuesEndpoint) but never imports it; add
the missing import "from django.urls import path" near the top of the module
(alongside any existing django.urls imports) so path is defined when Django
loads urlpatterns and the route declarations (e.g.,
TransferIssueEndpoint.as_view()) won't raise NameError.
| for child_issue in child_issues: | ||
| transfer_issue( | ||
| slug=slug, | ||
| source_project_id=source_project_id, | ||
| target_project_id=target_project_id, | ||
| issue_id=str(child_issue.id), | ||
| request=request, | ||
| user_id=user_id, | ||
| ) |
There was a problem hiding this comment.
Propagate descendant transfer results instead of returning only the root issue.
The recursive call ignores each child result, so a child failure still returns success for the parent and can leave a source-project child pointing at a target-project parent. On the success path, the response only includes the top-level issue_id, so callers cannot remove recursively moved descendants from source state and will keep stale sub-issues rendered until a full refetch.
Also applies to: 465-470
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/api/plane/utils/issue_transfer.py` around lines 418 - 426, The recursive
loop that calls transfer_issue for each child_issue ignores and discards each
child's result, so failures are not propagated and successful responses only
return the top-level issue_id; change the logic in the transfer_issue recursion
(the loop over child_issues that calls transfer_issue with slug,
source_project_id, target_project_id, issue_id=str(child_issue.id), request,
user_id) to collect each child's return value/result, aggregate success/failure
(e.g., accumulate moved_issue_ids and bubble up any failures or partial
failures), and include all descendant issue_ids in the final response; apply the
same fix to the other recursive call site around the block for lines 465-470 so
callers can remove recursively moved descendants and any child errors cause the
parent call to report failure.
| issue_activity.delay( | ||
| type="issue.activity.transferred", | ||
| requested_data=json.dumps(activity_data, cls=DjangoJSONEncoder), | ||
| actor_id=str(user_id), | ||
| issue_id=str(issue.id), | ||
| project_id=str(target_project_id), | ||
| current_instance=None, | ||
| epoch=int(timezone.now().timestamp()), | ||
| notification=True, | ||
| origin=base_host(request=request, is_app=True), | ||
| ) |
There was a problem hiding this comment.
Use a supported activity type here.
issue_activity.delay() is being called with type="issue.activity.transferred", but apps/api/plane/bgtasks/issue_activities_task.py:1537-1565 does not map that type. This transfer will therefore create no issue activity entry, so the audit trail for the new feature is silently missing.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/api/plane/utils/issue_transfer.py` around lines 453 - 463, The activity
call uses an unsupported type "issue.activity.transferred" so no activity entry
is created; fix by either switching the type passed to issue_activity.delay(...)
to one of the keys handled by the activity dispatch in issue_activities_task.py
(use the exact mapped string used there, e.g., the existing transfer-related
key) or add a handler in apps/api/plane/bgtasks/issue_activities_task.py that
maps "issue.activity.transferred" to create the transfer audit entry (reuse the
same payload fields: requested_data, actor_id, issue_id, project_id, epoch,
origin, etc.), and update/add tests to assert the transfer activity is created.
| for issue_id in issue_ids: | ||
| result = transfer_issue( | ||
| slug=slug, | ||
| source_project_id=source_project_id, | ||
| target_project_id=target_project_id, | ||
| issue_id=issue_id, | ||
| request=request, | ||
| user_id=user_id, | ||
| ) | ||
| results.append(result) |
There was a problem hiding this comment.
Skip descendants that were already moved by an earlier parent transfer.
transfer_issue() recursively moves child issues, but bulk_transfer_issues() still iterates every requested id independently. If the selection contains both a parent and one of its children, the parent iteration moves the child first and the later child iteration reports "Issue not found in source project", even though the transfer already succeeded.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/api/plane/utils/issue_transfer.py` around lines 506 - 515, The loop in
bulk_transfer_issues calls transfer_issue for every requested id even when a
parent transfer already moved its descendants; fix by tracking moved issue ids
and skipping subsequent calls for those ids. Implement a moved_ids set in
bulk_transfer_issues and, before calling transfer_issue(issue_id=...), skip if
issue_id in moved_ids; change transfer_issue to return the list/set of moved
issue IDs (or accept a moved_ids set to populate) and update the moved_ids set
with the returned/added ids after each successful transfer so children already
moved by a parent are not processed again.
| const createTransferToProjectMenuItem = (): TContextMenuItem => ({ | ||
| key: "transfer-to-project", | ||
| title: "Move to project", | ||
| icon: MoveRight, | ||
| action: () => handleOptionalAction(props.setTransferToProjectModal, "Move to project", true), | ||
| shouldRender: isEditingAllowed && (issueTypeDetail?.is_active ?? true), | ||
| }); |
There was a problem hiding this comment.
"Move to project" title is not passed through i18n.
All other action titles use t() (e.g., t("common.actions.edit"), t("common.actions.archive")). The hardcoded string will be untranslatable.
🌐 Proposed fix
const createTransferToProjectMenuItem = (): TContextMenuItem => ({
key: "transfer-to-project",
- title: "Move to project",
+ title: t("common.actions.move_to_project"),
icon: MoveRight,
action: () => handleOptionalAction(props.setTransferToProjectModal, "Move to project", true),
shouldRender: isEditingAllowed && (issueTypeDetail?.is_active ?? true),
});Add the corresponding key to the i18n translation files alongside the existing common.actions.* keys.
📝 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.
| const createTransferToProjectMenuItem = (): TContextMenuItem => ({ | |
| key: "transfer-to-project", | |
| title: "Move to project", | |
| icon: MoveRight, | |
| action: () => handleOptionalAction(props.setTransferToProjectModal, "Move to project", true), | |
| shouldRender: isEditingAllowed && (issueTypeDetail?.is_active ?? true), | |
| }); | |
| const createTransferToProjectMenuItem = (): TContextMenuItem => ({ | |
| key: "transfer-to-project", | |
| title: t("common.actions.move_to_project"), | |
| icon: MoveRight, | |
| action: () => handleOptionalAction(props.setTransferToProjectModal, "Move to project", true), | |
| shouldRender: isEditingAllowed && (issueTypeDetail?.is_active ?? true), | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@apps/web/core/components/issues/issue-layouts/quick-action-dropdowns/helper.tsx`
around lines 255 - 261, The menu item's hardcoded title in
createTransferToProjectMenuItem should be localized; replace the literal "Move
to project" with a call to the i18n function (e.g.,
t("common.actions.move_to_project")) where createTransferToProjectMenuItem
constructs the TContextMenuItem and ensure the new translation key
(common.actions.move_to_project) is added to the i18n translation files
alongside other common.actions.* keys so props.setTransferToProjectModal /
handleOptionalAction usage remains unchanged.
|
|
||
| export const TransferIssueToProjectModal = observer(function TransferIssueToProjectModal(props: Props) { | ||
| const { isOpen, handleClose, issueIds, sourceProjectId, onSuccess } = props; | ||
| const [query, setQuery] = useState(""); |
There was a problem hiding this comment.
Search query is not cleared when the modal closes.
query is never reset in handleClose. On the next open the stale search term persists, hiding projects the user previously filtered away. Reset query on close.
🐛 Proposed fix
const handleTransfer = async (targetProjectId: string) => {
// ...
try {
const response = await transferPromise;
if (didSucceed) {
+ setQuery("");
handleClose();
onSuccess?.();
}
} catch (error) {
console.error("Error transferring issues:", error);
}
};Alternatively, reset in a useEffect triggered by the isOpen prop:
+ useEffect(() => {
+ if (!isOpen) setQuery("");
+ }, [isOpen]);Also applies to: 85-87
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/core/components/issues/transfer-issue-to-project-modal.tsx` at line
28, The modal's search state (query) isn't reset on close; update the modal
close flow to clear it by calling setQuery("") when the modal closes (add this
in the existing handleClose function used to close the modal) or add a useEffect
that watches the isOpen prop and resets query to "" whenever isOpen becomes
false; ensure you update all close paths that call handleClose and remove any
stale query that would persist across opens (references: query, setQuery,
handleClose, isOpen).
| const { issues } = useIssues(EIssuesStoreType.PROJECT); | ||
|
|
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify that CYCLE/MODULE stores implement their own removeIssueFromList and
# do NOT subscribe to root-store removal events, confirming the stale-ID risk.
rg -n "removeIssueFromList\|removeIssue\b" \
--type=ts \
-g "!*.spec.*" \
-A2 -B2 \
apps/web/core/store/issue/Repository: makeplane/plane
Length of output: 41
🏁 Script executed:
# First, explore the store directory structure
find apps/web/core/store -type f -name "*.ts" | head -20Repository: makeplane/plane
Length of output: 946
🏁 Script executed:
# Also search for the modal file
find apps/web -type f -name "*transfer*" | grep -i modalRepository: makeplane/plane
Length of output: 185
🏁 Script executed:
# Search for store definitions by looking for issue store files
fd -e ts "issue" apps/web/core/storeRepository: makeplane/plane
Length of output: 1122
🏁 Script executed:
cat -n apps/web/core/components/issues/transfer-issue-to-project-modal.tsx | head -60Repository: makeplane/plane
Length of output: 2672
🏁 Script executed:
# Search for removeIssueFromList implementations in the stores
rg -n "removeIssueFromList" --type=ts apps/web/core/store/issue/Repository: makeplane/plane
Length of output: 2498
🏁 Script executed:
# Check for EIssuesStoreType enum definition
rg -n "enum EIssuesStoreType" --type=ts apps/web/core/Repository: makeplane/plane
Length of output: 41
🏁 Script executed:
# Find project issue store and check transfer methods
rg -n "transferIssue\|bulkTransferIssues" --type=ts -A 10 apps/web/core/store/issue/project/issue.store.ts | head -80Repository: makeplane/plane
Length of output: 41
🏁 Script executed:
# Also check the base store for these methods
rg -n "transferIssue\|bulkTransferIssues" --type=ts -A 10 apps/web/core/store/issue/helpers/base-issues.store.ts | head -100Repository: makeplane/plane
Length of output: 41
🏁 Script executed:
# Check where TransferIssueToProjectModal is called from
rg -n "TransferIssueToProjectModal" --type=ts apps/web/Repository: makeplane/plane
Length of output: 1734
🏁 Script executed:
cat -n apps/web/core/store/issue/project/issue.store.ts | head -100Repository: makeplane/plane
Length of output: 3947
🏁 Script executed:
# Search for transfer methods in all store files
rg -n "transferIssue" --type=ts apps/web/core/store/issue/Repository: makeplane/plane
Length of output: 1330
🏁 Script executed:
sed -n '785,850p' apps/web/core/store/issue/helpers/base-issues.store.tsRepository: makeplane/plane
Length of output: 1936
🏁 Script executed:
# Also search for bulkTransferIssues
rg -n "bulkTransferIssues" --type=ts -A 20 apps/web/core/store/issue/helpers/base-issues.store.tsRepository: makeplane/plane
Length of output: 1859
🏁 Script executed:
# Verify how removeIssueFromList works and if there's any cross-store cleanup
sed -n '1278,1295p' apps/web/core/store/issue/helpers/base-issues.store.tsRepository: makeplane/plane
Length of output: 936
🏁 Script executed:
# Check if rootIssueStore.issues.removeIssue also triggers any other store cleanup
rg -n "removeIssue\s*=" --type=ts -B 2 -A 15 apps/web/core/store/issue/root.store.ts | head -60Repository: makeplane/plane
Length of output: 41
🏁 Script executed:
# Find the removeIssue method in the root store
rg -n "removeIssue" --type=ts apps/web/core/store/issue/root.store.ts -A 8Repository: makeplane/plane
Length of output: 41
🏁 Script executed:
# Find root store file and explore its structure
find apps/web/core/store/issue -name "root.store.ts" -o -name "*root*"Repository: makeplane/plane
Length of output: 153
🏁 Script executed:
# Search for the issue root store and removeIssue
rg -n "class.*Issue.*Store\|removeIssue" --type=ts apps/web/core/store/issue/ | grep -E "(root|removeIssue)" | head -20Repository: makeplane/plane
Length of output: 41
🏁 Script executed:
# Let's check the cycle and module store implementations to confirm they have independent issue lists
sed -n '1,150p' apps/web/core/store/issue/cycle/issue.store.tsRepository: makeplane/plane
Length of output: 4777
🏁 Script executed:
# Check what properties these stores maintain
rg -n "this.issues\|issueIds" --type=ts apps/web/core/store/issue/cycle/issue.store.ts | head -20Repository: makeplane/plane
Length of output: 41
🏁 Script executed:
# Let's verify the cycle store maintains independent issue tracking
sed -n '300,330p' apps/web/core/store/issue/cycle/issue.store.tsRepository: makeplane/plane
Length of output: 900
🏁 Script executed:
# Check the updateIssueList method to understand how issue lists are maintained
sed -n '1290,1350p' apps/web/core/store/issue/helpers/base-issues.store.tsRepository: makeplane/plane
Length of output: 2858
Hardcoded EIssuesStoreType.PROJECT causes stale issue IDs in Cycle/Module stores after transfer.
The modal always calls issues.transferIssue / issues.bulkTransferIssues on the PROJECT store, regardless of which quick-actions menu opened it. The PROJECT store's removeIssueFromList removes the ID from the project list and rootIssueStore.issues.removeIssue removes the issue object globally — but the CYCLE and MODULE stores still hold the transferred issue's ID in their own tracked lists. After the transfer, cycle/module board cells will attempt to render an issue that no longer exists in the root store, resulting in empty slots or potential undefined-access errors until the next full reload.
The modal should accept the store context used by its caller so the correct store performs the transfer and removes the issue from its own list. Options:
- Add a
storeType: EIssuesStoreTypeprop toTransferIssueToProjectModaland useuseIssues(storeType)inside, then thread it from each caller. - Accept a
transferFn/bulkTransferFncallback prop from callers that already have the right store wired up.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/core/components/issues/transfer-issue-to-project-modal.tsx` around
lines 32 - 33, The modal always uses useIssues(EIssuesStoreType.PROJECT) which
causes transfers to operate on the PROJECT store only and leaves IDs stale in
CYCLE/MODULE stores; update TransferIssueToProjectModal to accept the caller's
store context and use that store for transfers: add a prop (storeType:
EIssuesStoreType) to TransferIssueToProjectModal (or alternatively accept
transferFn/bulkTransferFn props) and replace the hardcoded
useIssues(EIssuesStoreType.PROJECT) with useIssues(storeType) (or call the
injected transfer functions) so issues.transferIssue / issues.bulkTransferIssues
run on the correct store and its removeIssueFromList removes IDs from the
originating store rather than always touching the PROJECT store.
| try { | ||
| const response = await transferPromise; | ||
| if (response.success) { | ||
| handleClose(); | ||
| onSuccess?.(); | ||
| } | ||
| } catch (error) { | ||
| console.error("Error transferring issues:", error); | ||
| } |
There was a problem hiding this comment.
Modal never closes after a successful bulk transfer — response.success is absent on the bulk response.
For the single-transfer path, transferIssue in the store checks if (response.success) internally and the API returns { success: true, … }. For the bulk path, bulkTransferIssues in the store checks if (transferredIssues.length > 0) and returns the raw API response { transferred_issues: string[] } — no success field is present. As a result, response.success is undefined (falsy), so handleClose() is never invoked and the modal stays open even after setPromiseToast has already shown the success toast.
🐛 Proposed fix
try {
const response = await transferPromise;
- if (response.success) {
+ const didSucceed = isMultiple
+ ? (Array.isArray(response.transferred_issues) && response.transferred_issues.length > 0)
+ : !!response.success;
+ if (didSucceed) {
handleClose();
onSuccess?.();
}
} catch (error) {
console.error("Error transferring issues:", error);
}📝 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.
| try { | |
| const response = await transferPromise; | |
| if (response.success) { | |
| handleClose(); | |
| onSuccess?.(); | |
| } | |
| } catch (error) { | |
| console.error("Error transferring issues:", error); | |
| } | |
| try { | |
| const response = await transferPromise; | |
| const didSucceed = isMultiple | |
| ? (Array.isArray(response.transferred_issues) && response.transferred_issues.length > 0) | |
| : !!response.success; | |
| if (didSucceed) { | |
| handleClose(); | |
| onSuccess?.(); | |
| } | |
| } catch (error) { | |
| console.error("Error transferring issues:", error); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/core/components/issues/transfer-issue-to-project-modal.tsx` around
lines 82 - 90, The modal isn't closing because the bulk transfer response lacks
response.success; after awaiting transferPromise in the try block, detect
success for both single and bulk responses by checking either response.success
=== true OR (Array.isArray(response.transferred_issues) &&
response.transferred_issues.length > 0), and only then call handleClose() and
onSuccess?. Update the logic around transferPromise (the code that follows await
transferPromise) to normalize/branch on these two shapes (responses from
transferIssue vs bulkTransferIssues) so the modal closes after successful bulk
transfers.
| <button onClick={handleClose}> | ||
| <CloseIcon className="h-4 w-4" /> | ||
| </button> |
There was a problem hiding this comment.
Close button is missing an aria-label, making it inaccessible to screen readers.
The button renders only an icon with no accessible name. Assistive technology will announce it with no context.
♿ Proposed fix
- <button onClick={handleClose}>
+ <button onClick={handleClose} type="button" aria-label="Close modal">
<CloseIcon className="h-4 w-4" />
</button>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/core/components/issues/transfer-issue-to-project-modal.tsx` around
lines 103 - 105, The close button rendering only <CloseIcon /> lacks an
accessible name; update the button that invokes handleClose to include an ARIA
label (e.g., aria-label="Close" or aria-label={`Close ${modalTitle}`} for
context) and ensure it has type="button" if missing so screen readers can
announce its purpose; locate the button element wrapping CloseIcon in the
transfer-issue-to-project modal and add the aria-label attribute (or
aria-labelledby pointing to a visible label) accordingly.
| transferIssue = async ( | ||
| workspaceSlug: string, | ||
| sourceProjectId: string, | ||
| targetProjectId: string, | ||
| data: { issue_ids: string[] } | ||
| ) => { | ||
| const currentStore = | ||
| this.serviceType === EIssueServiceType.EPICS | ||
| ? this.rootIssueDetailStore.rootIssueStore.projectEpics | ||
| : this.rootIssueDetailStore.rootIssueStore.projectIssues; | ||
| await currentStore.transferIssue(workspaceSlug, sourceProjectId, targetProjectId, data); | ||
| }; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -name "base-issues.store.ts" -type fRepository: makeplane/plane
Length of output: 164
🏁 Script executed:
find . -name "issue.store.ts" -type f | grep -E "issue-details"Repository: makeplane/plane
Length of output: 116
🏁 Script executed:
cd apps/web && find . -path "*/core/store/issue*" -name "*.ts" | head -20Repository: makeplane/plane
Length of output: 1024
🏁 Script executed:
sed -n '110,130p' ./apps/web/core/store/issue/helpers/base-issues.store.tsRepository: makeplane/plane
Length of output: 629
🏁 Script executed:
sed -n '210,230p' ./apps/web/core/store/issue/issue-details/issue.store.tsRepository: makeplane/plane
Length of output: 929
🏁 Script executed:
sed -n '1,50p' ./apps/web/core/store/issue/issue-details/issue.store.ts | head -40Repository: makeplane/plane
Length of output: 1971
🏁 Script executed:
rg "transferIssue" ./apps/web/core/store/issue/issue-details/issue.store.ts -B 2 -A 2Repository: makeplane/plane
Length of output: 644
🏁 Script executed:
sed -n '114,150p' ./apps/web/core/store/issue/helpers/base-issues.store.tsRepository: makeplane/plane
Length of output: 1147
🏁 Script executed:
rg "transferIssue\s*=" ./apps/web/core/store/issue/helpers/base-issues.store.ts -A 15 | head -40Repository: makeplane/plane
Length of output: 583
Use the bulk transfer method for multi-issue operations or pass a single issue ID.
The detail store's transferIssue method passes an object { issue_ids: string[] } to the base store's transferIssue, which expects a single issueId: string as the fourth parameter. This causes the API to receive { issue_id: { issue_ids: [...] } } instead of a proper issue ID string.
Since this is a detail store and the method accepts an array of IDs, call currentStore.bulkTransferIssues(workspaceSlug, sourceProjectId, targetProjectId, data.issue_ids) instead, or pass a single issue ID if this operation is meant for single-issue contexts.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/core/store/issue/issue-details/issue.store.ts` around lines 215 -
226, The transferIssue method in issue.store.ts is passing an object {
issue_ids: string[] } to currentStore.transferIssue which expects a single
issueId string; update this to call
currentStore.bulkTransferIssues(workspaceSlug, sourceProjectId, targetProjectId,
data.issue_ids) instead (or if the intent is single-issue, extract and pass
data.issue_ids[0] to currentStore.transferIssue); locate the method around
transferIssue, currentStore (derived from
rootIssueDetailStore.rootIssueStore.projectEpics or projectIssues), and change
the call to use bulkTransferIssues with data.issue_ids.
Summary by CodeRabbit
Release Notes
New Features