Skip to content

.1502411993651747:9465261c10d0fa2600e994c8bda80fc6_69f5fb46dc48703ced6cfd4e.69f5fb52dc48703ced6cfd53.69f5fb510b407201bc3fbc0e:Trae CN.T(2026/5/2 21:25:38)#8993

Open
lovely90133 wants to merge 2 commits intomakeplane:previewfrom
lovely90133:trae3

Conversation

@lovely90133
Copy link
Copy Markdown

@lovely90133 lovely90133 commented May 2, 2026

Summary by CodeRabbit

Release Notes

New Features

  • Transfer single or multiple issues between projects within the same workspace
  • "Move to project" quick action available in issue context menus
  • Automatic mapping of issue states, labels, and assignees during transfers
  • Transfer preserves related data including comments, attachments, and linked items

@CLAassistant
Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 2, 2026

📝 Walkthrough

Walkthrough

Introduces 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.

Changes

Issue Transfer Across Projects

Layer / File(s) Summary
Backend Routing
apps/api/plane/app/urls/issue.py, apps/api/plane/app/views/__init__.py
Two new URL routes (transfer-issue/ and bulk-transfer-issues/) are registered and wired to TransferIssueEndpoint and BulkTransferIssuesEndpoint view classes.
Backend API Endpoints
apps/api/plane/app/views/issue/base.py
TransferIssueEndpoint and BulkTransferIssuesEndpoint POST handlers validate source/target project IDs, reject same-project transfers, delegate to utility functions, and return appropriately typed responses (200 on success, 207 Multi-Status for partial bulk failures, 400 on error).
Backend Transfer Logic
apps/api/plane/utils/issue_transfer.py
Core transfer_issue performs atomic transaction: permission checks, state mapping (with fallback resolution), label/assignee remapping by name/eligibility, clears cycle/module links, updates issue record, recreates issue relations, migrates all related entities (comments, reactions, links, mentions, blockers, subscribers, votes, files), recursively transfers child issues, updates sequencing and recent visits, and enqueues async activity logging. bulk_transfer_issues iterates per issue and aggregates results.
Frontend Service Layer
apps/web/core/services/issue/issue.service.ts
transferIssue and bulkTransferIssues methods POST to backend endpoints and return typed transfer summaries (success flag, transferred issue IDs, error details).
Frontend Store Operations
apps/web/core/store/issue/helpers/base-issues.store.ts, apps/web/core/store/issue/issue-details/issue.store.ts, apps/web/core/store/issue/issue-details/root.store.ts
Store implementations delegate to service, perform optimistic source-project cleanup (remove transferred issues, update parent stats), attempt to retrieve transferred issue details from target project, and expose transferIssue/bulkTransferIssues through store hierarchy.
Frontend UI Components
apps/web/core/components/issues/transfer-issue-to-project-modal.tsx
New modal component for selecting target project: renders filtered project list by search, handles single vs. bulk transfers with pluralized messaging, closes and calls onSuccess callback on completion.
Frontend Menu Integration
apps/web/core/components/issues/issue-layouts/quick-action-dropdowns/helper.tsx, apps/web/core/components/issues/issue-layouts/quick-action-dropdowns/*-issue.tsx
"Move to project" menu item factory added to helper; wired into all quick-action dropdown contexts (project, work-item-detail, all-issue, cycle-issue, module-issue). Each component adds transferToProjectModal state and conditional modal rendering.
Frontend Issue Detail
apps/web/core/components/issues/issue-detail/root.tsx
TIssueOperations gains optional transfer method; issueOperations memoized object wires it to the issue detail store's transferIssue action.

Sequence Diagram

sequenceDiagram
    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
Loading

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

🐇 A rabbit hops between project grounds,
With issue bundles safe and sound,
State maps true, labels follow fast,
Child issues find their new home at last!
Transfer complete—now watch them grow,
In projects fresh, from head to toe! 🌱

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (3 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The pull request title is a garbled string with timestamps and encoded characters that does not convey any meaningful information about the changeset, making it impossible to understand the primary change. Replace the title with a clear, concise summary of the main change, such as 'Add issue transfer functionality between projects' or 'Implement issue transfer endpoints and UI components'.
Description check ⚠️ Warning The pull request has no description provided, while the repository template requires sections for Description, Type of Change, Test Scenarios, and References. Add a comprehensive description following the repository template, including the purpose of changes, type of change classification, test scenarios performed, and any related issue references.
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 7/8 reviews remaining, refill in 7 minutes and 30 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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

📥 Commits

Reviewing files that changed from the base of the PR and between a62fe8a and a96188d.

📒 Files selected for processing (16)
  • apps/api/plane/app/urls/issue.py
  • apps/api/plane/app/views/__init__.py
  • apps/api/plane/app/views/issue/base.py
  • apps/api/plane/utils/issue_transfer.py
  • apps/web/core/components/issues/issue-detail/root.tsx
  • apps/web/core/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx
  • apps/web/core/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx
  • apps/web/core/components/issues/issue-layouts/quick-action-dropdowns/helper.tsx
  • apps/web/core/components/issues/issue-layouts/quick-action-dropdowns/issue-detail.tsx
  • apps/web/core/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx
  • apps/web/core/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx
  • apps/web/core/components/issues/transfer-issue-to-project-modal.tsx
  • apps/web/core/services/issue/issue.service.ts
  • apps/web/core/store/issue/helpers/base-issues.store.ts
  • apps/web/core/store/issue/issue-details/issue.store.ts
  • apps/web/core/store/issue/issue-details/root.store.ts

Comment on lines +286 to +295
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",
),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

cat -n "apps/api/plane/app/urls/issue.py" | head -30

Repository: 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 -50

Repository: 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 -10

Repository: makeplane/plane

Length of output: 322


🏁 Script executed:

rg "^from django.urls import" "apps/api/plane/app/views" --max-count 3

Repository: makeplane/plane

Length of output: 124


🏁 Script executed:

sed -n '1,36p' "apps/api/plane/app/urls/issue.py" | tail -5

Repository: makeplane/plane

Length of output: 137


🏁 Script executed:

grep -n "path" "apps/api/plane/app/urls/issue.py" | head -3

Repository: 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 -20

Repository: 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()}")
EOF

Repository: 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}")
EOF

Repository: 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.

Comment on lines +418 to +426
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,
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

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.

Comment on lines +453 to +463
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),
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Comment on lines +506 to +515
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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Comment on lines +255 to +261
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),
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

"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.

Suggested change
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("");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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).

Comment on lines +32 to +33
const { issues } = useIssues(EIssuesStoreType.PROJECT);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/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 -20

Repository: makeplane/plane

Length of output: 946


🏁 Script executed:

# Also search for the modal file
find apps/web -type f -name "*transfer*" | grep -i modal

Repository: 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/store

Repository: makeplane/plane

Length of output: 1122


🏁 Script executed:

cat -n apps/web/core/components/issues/transfer-issue-to-project-modal.tsx | head -60

Repository: 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 -80

Repository: 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 -100

Repository: 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 -100

Repository: 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.ts

Repository: 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.ts

Repository: 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.ts

Repository: 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 -60

Repository: 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 8

Repository: 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 -20

Repository: 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.ts

Repository: 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 -20

Repository: 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.ts

Repository: 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.ts

Repository: 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:

  1. Add a storeType: EIssuesStoreType prop to TransferIssueToProjectModal and use useIssues(storeType) inside, then thread it from each caller.
  2. Accept a transferFn / bulkTransferFn callback 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.

Comment on lines +82 to +90
try {
const response = await transferPromise;
if (response.success) {
handleClose();
onSuccess?.();
}
} catch (error) {
console.error("Error transferring issues:", error);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Suggested change
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.

Comment on lines +103 to +105
<button onClick={handleClose}>
<CloseIcon className="h-4 w-4" />
</button>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

Comment on lines +215 to +226
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);
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

find . -name "base-issues.store.ts" -type f

Repository: 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 -20

Repository: makeplane/plane

Length of output: 1024


🏁 Script executed:

sed -n '110,130p' ./apps/web/core/store/issue/helpers/base-issues.store.ts

Repository: makeplane/plane

Length of output: 629


🏁 Script executed:

sed -n '210,230p' ./apps/web/core/store/issue/issue-details/issue.store.ts

Repository: makeplane/plane

Length of output: 929


🏁 Script executed:

sed -n '1,50p' ./apps/web/core/store/issue/issue-details/issue.store.ts | head -40

Repository: makeplane/plane

Length of output: 1971


🏁 Script executed:

rg "transferIssue" ./apps/web/core/store/issue/issue-details/issue.store.ts -B 2 -A 2

Repository: makeplane/plane

Length of output: 644


🏁 Script executed:

sed -n '114,150p' ./apps/web/core/store/issue/helpers/base-issues.store.ts

Repository: makeplane/plane

Length of output: 1147


🏁 Script executed:

rg "transferIssue\s*=" ./apps/web/core/store/issue/helpers/base-issues.store.ts -A 15 | head -40

Repository: 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants