Skip to content

[PLAN-90] fix: persist page list sort preference after reload#9282

Open
alanssant0s wants to merge 19 commits into
makeplane:previewfrom
alanssant0s:fix/plan-90-persist-page-sort
Open

[PLAN-90] fix: persist page list sort preference after reload#9282
alanssant0s wants to merge 19 commits into
makeplane:previewfrom
alanssant0s:fix/plan-90-persist-page-sort

Conversation

@alanssant0s

@alanssant0s alanssant0s commented Jun 21, 2026

Copy link
Copy Markdown

Description

A ordenação da lista de Pages (nome, data de criação, data de modificação) não era persistida — ao recarregar a página, voltava sempre para Date modified.

Esta PR salva sortKey e sortBy no localStorage por projeto (page_display_filters), seguindo o mesmo padrão do ModuleFilterStore. A preferência é restaurada ao abrir o projeto e atualizada sempre que o usuário altera a ordenação.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • Feature (non-breaking change which adds functionality)
  • Improvement (change that would cause existing functionality to not work as expected)
  • Code refactoring
  • Performance improvements
  • Documentation update

Screenshots and Media (if applicable)

Test Scenarios

  • Abrir a lista de Pages de um projeto e confirmar que o padrão inicial é Date modified
  • Alterar a ordenação para Name e verificar que a lista reordena corretamente
  • Recarregar a página (F5) e confirmar que a ordenação permanece em Name
  • Trocar para outro projeto, definir outra ordenação e voltar ao projeto original — cada projeto deve manter sua preferência
  • Alterar entre asc/desc e recarregar — a direção da ordenação também deve persistir

References

  • PLAN-90

Claude Code session

Made with Cursor

Summary by CodeRabbit

Release Notes

New Features

  • Added workspace-level Sprints/Squads management system with automation and scheduled sprint creation capabilities
  • Enabled assigning issues to workspace sprints and filtering by sprint across all layouts
  • Added sprint navigation in sidebar with drag-and-drop reordering
  • Introduced sprint archive/restore functionality
  • Added sprint-aware display filters and properties in spreadsheet, calendar, list, and Kanban views
  • Added Docker Compose production deployment configuration

Documentation

  • Added Portuguese production Docker deployment guide with environment setup and rollback procedures

alanssant0s and others added 17 commits June 19, 2026 14:51
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
feat: add workspace sprint planning
- Added serializers for WorkspaceSprintSquad and WorkspaceSprintAutomationMember.
- Updated views and URLs to support CRUD operations for squads and their members.
- Introduced access control for sprint automation based on user roles.
- Enhanced the database models to include new fields for squad automation and member management.
- Updated frontend components to reflect the new squad structure and navigation preferences.
Add routeFilters support and sprint-aware display handling. Introduces routeFilters and setRouteFilters in WorkspaceIssuesFilter, uses global_sprint_id to pick the correct layout/filter handling in getAppliedFilters, and wires setRouteFilters calls in WorkspaceIssues when fetching/paginating so route filters are applied. Update calendar IssueBlock to read workspaceSprintId, compute displayProperties (forcing project=true and sprint=false for sprint routes) via getComputedDisplayProperties, and pass the adjusted displayProperties to ControlLink. These changes ensure sprint routes render expected fields and filters are respected during fetch and pagination.
feat: add sprint squads and route filters
Co-authored-by: Cursor <cursoragent@cursor.com>
ci: deploy Dokploy after image promotion
- Removed "Join our Forum" links from the sidebar help section and error page.
- Deleted the "Star Us on GitHub" component and its references.
- Cleaned up unused imports and components related to product updates and forum links across various files.
- Updated translations to reflect the removal of forum-related text in multiple languages.
- Introduced grouping and sub-grouping capabilities in the WorkspaceViewIssuesViewSet, allowing users to group issues based on specified parameters.
- Added validation to prevent identical group and sub-group parameters.
- Updated pagination logic to support new grouping features using GroupedOffsetPaginator and SubGroupedOffsetPaginator.
- Enhanced issue data structure by including global sprint identifiers in the grouper utility.
- Modified various components to utilize fallback names for sprints, improving user experience in dropdowns and issue layouts.
- Refactored filter store to accommodate new global sprint ID handling for workspace views.
- Resolved bugs related to pagination when grouping issues, ensuring accurate data retrieval.
- Improved validation logic to handle edge cases in group and sub-group parameters.
- Adjusted the integration of GroupedOffsetPaginator and SubGroupedOffsetPaginator for better performance.
- Enhanced user experience by refining dropdowns and layouts to reflect the latest changes in issue grouping.
feat: improve sprint issue grouping
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
feat: add assigned-to-me issue filtering
Co-authored-by: Cursor <cursoragent@cursor.com>
feat: expose project pages via API v1
@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

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Too many files!

This PR contains 153 files, which is 3 over the limit of 150.

To get a review, narrow the scope:
• coderabbit review --type committed # exclude uncommitted changes
• coderabbit review --dir # limit to a subdirectory
• coderabbit review --base # compare against a closer base

Upgrade to a paid plan to raise the limit.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 767916d1-b17d-4c89-8216-24e0da587ee9

📥 Commits

Reviewing files that changed from the base of the PR and between 6db63a5 and 16076c5.

📒 Files selected for processing (153)
  • .cursor/rules/pre-commit-validations.mdc
  • apps/admin/app/(all)/(dashboard)/sidebar-help-section.tsx
  • apps/api/plane/api/urls/__init__.py
  • apps/api/plane/api/urls/page.py
  • apps/api/plane/api/views/page.py
  • apps/api/plane/app/views/analytic/advance.py
  • apps/api/plane/app/views/page/base.py
  • apps/api/plane/app/views/view/base.py
  • apps/api/plane/bgtasks/workspace_seed_task.py
  • apps/api/plane/db/models/cycle.py
  • apps/api/plane/db/models/issue.py
  • apps/api/plane/db/models/module.py
  • apps/api/plane/db/models/project.py
  • apps/api/plane/db/models/view.py
  • apps/api/plane/db/models/workspace.py
  • apps/api/plane/seeds/data/views.json
  • apps/api/plane/utils/date_utils.py
  • apps/api/plane/utils/grouper.py
  • apps/space/app/error.tsx
  • apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/header.tsx
  • apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/layout.tsx
  • apps/web/app/(all)/[workspaceSlug]/(projects)/sprints/header.tsx
  • apps/web/app/(all)/[workspaceSlug]/(projects)/sprints/layout.tsx
  • apps/web/app/(all)/[workspaceSlug]/(projects)/star-us-link.tsx
  • apps/web/app/(all)/workspace-invitations/page.tsx
  • apps/web/ce/components/global/product-updates/changelog.tsx
  • apps/web/ce/components/global/product-updates/header.tsx
  • apps/web/ce/components/issues/header.tsx
  • apps/web/ce/components/navigations/top-navigation-root.tsx
  • apps/web/ce/components/workspace/billing/root.tsx
  • apps/web/ce/components/workspace/edition-badge.tsx
  • apps/web/ce/components/workspace/sidebar/helper.tsx
  • apps/web/ce/hooks/pages/use-extended-editor-extensions.ts
  • apps/web/core/components/analytics/total-insights.tsx
  • apps/web/core/components/analytics/work-items/created-vs-resolved.tsx
  • apps/web/core/components/analytics/work-items/modal/content.tsx
  • apps/web/core/components/analytics/work-items/modal/index.tsx
  • apps/web/core/components/analytics/work-items/priority-chart.tsx
  • apps/web/core/components/analytics/work-items/workitems-insight-table.tsx
  • apps/web/core/components/common/latest-feature-block.tsx
  • apps/web/core/components/dropdowns/workspace-sprint/index.tsx
  • apps/web/core/components/dropdowns/workspace-sprint/sprint-options.tsx
  • apps/web/core/components/editor/document/editor.tsx
  • apps/web/core/components/global/index.ts
  • apps/web/core/components/global/product-updates/fallback.tsx
  • apps/web/core/components/global/product-updates/footer.tsx
  • apps/web/core/components/global/product-updates/modal.tsx
  • apps/web/core/components/home/home-dashboard-widgets.tsx
  • apps/web/core/components/issues/filters.tsx
  • apps/web/core/components/issues/issue-layouts/filters/header/display-filters/display-filters-selection.tsx
  • apps/web/core/components/issues/issue-layouts/filters/header/display-filters/display-properties.tsx
  • apps/web/core/components/issues/issue-layouts/properties/all-properties.tsx
  • apps/web/core/components/issues/issue-layouts/roots/all-issue-layout-root.tsx
  • apps/web/core/components/issues/issue-layouts/spreadsheet/columns/header-column.tsx
  • apps/web/core/components/issues/issue-layouts/spreadsheet/columns/sprint-column.tsx
  • apps/web/core/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx
  • apps/web/core/components/issues/issue-layouts/utils.tsx
  • apps/web/core/components/issues/me-mode-toggle.tsx
  • apps/web/core/components/power-k/config/help-commands.ts
  • apps/web/core/components/power-k/ui/pages/open-entity/workspace-settings-menu.tsx
  • apps/web/core/components/settings/workspace/sidebar/item-categories.tsx
  • apps/web/core/components/workspace/sidebar/help-section/root.tsx
  • apps/web/core/constants/plans.tsx
  • apps/web/core/services/analytics.service.ts
  • apps/web/core/services/workspace.service.ts
  • apps/web/core/store/analytics.store.ts
  • apps/web/core/store/issue/cycle/filter.store.ts
  • apps/web/core/store/issue/helpers/issue-filter-helper.store.ts
  • apps/web/core/store/issue/project/filter.store.ts
  • apps/web/core/store/issue/workspace/filter.store.ts
  • apps/web/core/store/pages/project-page.store.ts
  • apps/web/helpers/page-links.ts
  • packages/codemods/tests/function-declaration.spec.ts
  • packages/constants/src/event-tracker/core.ts
  • packages/constants/src/issue/common.ts
  • packages/constants/src/settings/workspace.ts
  • packages/editor/src/ce/extensions/document-extensions.tsx
  • packages/editor/src/ce/extensions/rich-text-extensions.tsx
  • packages/editor/src/ce/extensions/slash-commands.tsx
  • packages/editor/src/ce/types/editor-extended.ts
  • packages/editor/src/core/components/menus/bubble-menu/link-selector.tsx
  • packages/editor/src/core/components/menus/bubble-menu/root.tsx
  • packages/editor/src/core/components/menus/page-link-picker-overlay.tsx
  • packages/editor/src/core/components/menus/page-link-search-panel.tsx
  • packages/editor/src/core/extensions/custom-link/extension.tsx
  • packages/editor/src/core/extensions/custom-link/helpers/clickHandler.ts
  • packages/editor/src/core/extensions/extensions.ts
  • packages/editor/src/core/extensions/slash-commands/command-items-list.tsx
  • packages/editor/src/core/extensions/slash-commands/root.tsx
  • packages/editor/src/core/helpers/editor-commands.ts
  • packages/editor/src/core/helpers/open-page-link-picker.ts
  • packages/i18n/src/locales/cs/common.json
  • packages/i18n/src/locales/cs/home.json
  • packages/i18n/src/locales/cs/power-k.json
  • packages/i18n/src/locales/de/common.json
  • packages/i18n/src/locales/de/home.json
  • packages/i18n/src/locales/de/power-k.json
  • packages/i18n/src/locales/en/common.json
  • packages/i18n/src/locales/en/home.json
  • packages/i18n/src/locales/en/power-k.json
  • packages/i18n/src/locales/es/common.json
  • packages/i18n/src/locales/es/home.json
  • packages/i18n/src/locales/es/power-k.json
  • packages/i18n/src/locales/fr/common.json
  • packages/i18n/src/locales/fr/home.json
  • packages/i18n/src/locales/fr/power-k.json
  • packages/i18n/src/locales/id/common.json
  • packages/i18n/src/locales/id/home.json
  • packages/i18n/src/locales/id/power-k.json
  • packages/i18n/src/locales/it/common.json
  • packages/i18n/src/locales/it/home.json
  • packages/i18n/src/locales/it/power-k.json
  • packages/i18n/src/locales/ja/common.json
  • packages/i18n/src/locales/ja/home.json
  • packages/i18n/src/locales/ja/power-k.json
  • packages/i18n/src/locales/ko/common.json
  • packages/i18n/src/locales/ko/home.json
  • packages/i18n/src/locales/ko/power-k.json
  • packages/i18n/src/locales/pl/common.json
  • packages/i18n/src/locales/pl/home.json
  • packages/i18n/src/locales/pl/power-k.json
  • packages/i18n/src/locales/pt-BR/common.json
  • packages/i18n/src/locales/pt-BR/home.json
  • packages/i18n/src/locales/pt-BR/power-k.json
  • packages/i18n/src/locales/ro/common.json
  • packages/i18n/src/locales/ro/home.json
  • packages/i18n/src/locales/ro/power-k.json
  • packages/i18n/src/locales/ru/common.json
  • packages/i18n/src/locales/ru/home.json
  • packages/i18n/src/locales/ru/power-k.json
  • packages/i18n/src/locales/sk/common.json
  • packages/i18n/src/locales/sk/home.json
  • packages/i18n/src/locales/sk/power-k.json
  • packages/i18n/src/locales/tr-TR/common.json
  • packages/i18n/src/locales/tr-TR/home.json
  • packages/i18n/src/locales/tr-TR/power-k.json
  • packages/i18n/src/locales/ua/common.json
  • packages/i18n/src/locales/ua/home.json
  • packages/i18n/src/locales/ua/power-k.json
  • packages/i18n/src/locales/vi-VN/common.json
  • packages/i18n/src/locales/vi-VN/home.json
  • packages/i18n/src/locales/vi-VN/power-k.json
  • packages/i18n/src/locales/zh-CN/common.json
  • packages/i18n/src/locales/zh-CN/home.json
  • packages/i18n/src/locales/zh-CN/power-k.json
  • packages/i18n/src/locales/zh-TW/common.json
  • packages/i18n/src/locales/zh-TW/home.json
  • packages/i18n/src/locales/zh-TW/power-k.json
  • packages/types/src/analytics.ts
  • packages/types/src/home.ts
  • packages/types/src/view-props.ts
  • packages/types/src/workspace.ts
  • packages/utils/src/work-item/base.ts

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds workspace sprints and squads across the API, web app, shared packages, and issue views, plus image build/promotion workflows, production Compose files, deployment docs, and repository review/deploy rules.

Changes

Workspace sprints and squads

Layer / File(s) Summary
Sprint schema and shared contracts
apps/api/plane/db/models/sprint.py, apps/api/plane/db/migrations/*, packages/types/src/sprint/*, packages/types/src/view-props.ts, packages/constants/src/issue/common.ts, packages/services/src/sprint/*
Adds sprint, squad/automation, member, and sprint-issue data models and migrations, plus shared types, display-property metadata, filter fields, navigation preferences, and service exports.
API endpoints and automation processing
apps/api/plane/app/views/workspace/sprint.py, apps/api/plane/app/serializers/sprint.py, apps/api/plane/bgtasks/workspace_sprint_task.py, apps/api/plane/app/views/issue/base.py, apps/api/plane/utils/filters/*, apps/api/plane/tests/contract/app/test_workspace_sprints.py
Adds workspace sprint CRUD, squad membership and archive endpoints, automated sprint generation, issue sprint annotations/filtering, and API contract tests.
Sprint store and route-driven fetching
apps/web/core/store/workspace-sprint.store.ts, apps/web/core/services/sprint.service.ts, apps/web/core/hooks/use-issues-actions.tsx, apps/web/core/store/issue/workspace/*, apps/web/app/routes/core.ts, apps/web/app/(all)/[workspaceSlug]/(projects)/sprints/*, apps/web/app/(all)/[workspaceSlug]/(projects)/squads/*
Adds the web sprint store and service, wires sprint routes and pages, and makes workspace issue fetching accept route-level sprint filters.
Squads navigation and management UI
apps/web/core/components/workspace/sidebar/sprints-list.tsx, apps/web/app/(all)/[workspaceSlug]/(projects)/sidebar.tsx, apps/web/core/components/workspace/sprints/*, apps/web/core/components/archives/workspace-archives-root.tsx, apps/web/core/components/navigation/customize-navigation-dialog.tsx, packages/constants/src/workspace.ts
Adds pinned sidebar squads navigation, squad and sprint management screens and modals, workspace archives support for squads, and navigation customization for sprint display mode and limits.
Issue layouts and sprint editing
apps/web/core/components/dropdowns/workspace-sprint/*, apps/web/core/components/issues/issue-layouts/..., apps/web/ce/components/issues/issue-layouts/utils.tsx, packages/utils/src/work-item/base.ts, packages/types/src/issues/issue.ts
Adds sprint/project display properties and spreadsheet columns, a workspace sprint dropdown for issue editing, sprint-aware workspace/global layouts, and persisted sprint fields on issue data.
Supporting web adjustments
apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/archives/page.tsx, apps/web/core/store/pages/project-page.store.ts, apps/web/core/store/issue/helpers/issue-filter-helper.store.ts
Updates archives routing and supporting store behavior used alongside the new workspace sprint flows.

Deployment and publishing automation

Layer / File(s) Summary
Image pipeline and production deployment
.github/workflows/docker-images.yml, docker-compose.image.yml, docker-compose.prod.yml, docs/production-docker.md
Adds image build/publish and main-branch promotion workflows, Dokploy deployment triggering, production Compose manifests, and deployment documentation.
Repository workflow rules
.cursor/rules/pr-deploy-publicacao.mdc, .gitignore
Adds PR/deploy review rules and expands ignored Python virtualenv and cache patterns.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant WebApp
  participant SprintAPI
  participant SprintStore

  User->>WebApp: Open squad or sprint route
  WebApp->>SprintStore: fetch squads and sprints
  SprintStore->>SprintAPI: list automations and sprints
  SprintAPI-->>SprintStore: sprint data
  SprintStore-->>WebApp: selected squad and sprint data
  User->>WebApp: Assign issue to sprint
  WebApp->>SprintStore: addIssueToSprint
  SprintStore->>SprintAPI: POST sprint issue link
  SprintAPI-->>SprintStore: sprint issue response
  SprintStore-->>WebApp: updated issue sprint fields
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Suggested reviewers

  • sriramveeraghanta
  • prateekshourya29

Poem

🐇 I hopped through squads and sprints today,
with routes and stores all lined in play.
New images flew to tags on high,
while archives winked as squads went by.
Carrot cheers for code in stride!

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (3 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The PR title references PLAN-90 and mentions persisting page list sort preference, but the raw summary shows extensive sprint/squad features being added, not just a page list sort fix. The title does not match the actual changeset scope. Either update the title to reflect the full scope of changes (sprints, squads, automation, etc.) or verify the correct PR is being analyzed.
Description check ⚠️ Warning The PR description is entirely in Portuguese and describes only a page list sort preference fix (PLAN-90), but the actual changeset contains massive sprint/squad infrastructure additions with no mention of this feature. The description is misaligned with the actual changes. Provide a comprehensive description covering all sprint, squad, automation, and infrastructure changes, or clarify if the wrong PR description was provided.
Docstring Coverage ⚠️ Warning Docstring coverage is 5.19% 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

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

alanssant0s and others added 2 commits June 21, 2026 17:59
Restore page-link support in the Pages editor and expose it via a new
Page link slash command with a searchable picker overlay.

Co-authored-by: Cursor <cursoragent@cursor.com>
Save sortKey and sortBy per project so the Pages list keeps the user's
ordering choice after reload instead of resetting to date modified.

Co-authored-by: Cursor <cursoragent@cursor.com>
@alanssant0s alanssant0s force-pushed the fix/plan-90-persist-page-sort branch from 6db63a5 to 16076c5 Compare June 21, 2026 21:02

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

🟠 Major comments (23)
docs/production-docker.md-46-46 (1)

46-46: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not include localhost in the production CORS example.

The production example currently whitelists http://127.0.0.1. Keep production examples restricted to real HTTPS origins and move localhost to a separate local-dev note.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/production-docker.md` at line 46, Remove the localhost address
http://127.0.0.1 from the CORS_ALLOWED_ORIGINS configuration in the production
Docker example to keep production configuration restricted to real HTTPS origins
only. Create a separate section or note specifically for local development that
documents how to configure CORS with localhost for development environments,
keeping production and development configuration examples clearly separated.
.github/workflows/docker-images.yml-18-21 (1)

18-21: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Scope packages: write to publishing jobs only.

packages: write at workflow scope grants write-capable token permissions to every job, including deploy-dokploy, which does not publish images. Move package-write permission to build-and-push and promote-main only.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/docker-images.yml around lines 18 - 21, The workflow-level
permissions block is granting packages:write access to all jobs including
deploy-dokploy which does not publish images. Move the packages:write permission
from the workflow-level permissions block (keeping only contents:read at
workflow scope) to job-level permissions blocks for the build-and-push and
promote-main jobs only. This ensures the deploy-dokploy job and any other
non-publishing jobs do not receive unnecessary write-capable token permissions.

Source: Linters/SAST tools

.github/workflows/docker-images.yml-172-175 (1)

172-175: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add timeout/retry guards for the Dokploy API call.

This external call has no timeout or retry policy; transient network stalls can block deployment for the full job timeout window.

Suggested fix
-          curl --fail-with-body --request POST "${DOKPLOY_URL}/api/compose.deploy" \
+          curl --fail-with-body --silent --show-error \
+            --connect-timeout 10 --max-time 60 \
+            --retry 3 --retry-all-errors --retry-delay 2 \
+            --request POST "${DOKPLOY_URL}/api/compose.deploy" \
             --header "Content-Type: application/json" \
             --header "x-api-key: ${DOKPLOY_API_KEY}" \
             --data "{\"composeId\":\"${DOKPLOY_COMPOSE_ID}\"}"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/docker-images.yml around lines 172 - 175, The curl command
making the POST request to the Dokploy API endpoint has no timeout or retry
policy, which can cause the deployment job to hang indefinitely during transient
network issues. Add a timeout parameter to the curl command using the --max-time
flag to set a reasonable timeout (e.g., 30 seconds), and add retry parameters
using --retry and --retry-max-time flags to automatically retry the request on
transient failures. Alternatively, wrap the curl command in a retry loop with
exponential backoff to handle temporary network stalls gracefully.
docker-compose.image.yml-149-170 (1)

149-170: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

TRUSTED_PROXIES and FILE_SIZE_LIMIT are currently ignored.

The generated Caddyfile hardcodes trusted_proxies static 0.0.0.0/0 and max_size 5242880, so the env values at Line 151 and Line 153 never take effect. This leaves proxy trust overly permissive and disables runtime limit tuning.

Suggested fix
-        cat > /tmp/Caddyfile <<'EOF'
+        cat > /tmp/Caddyfile <<EOF
         {
           servers {
             max_header_size 25MB
             client_ip_headers X-Forwarded-For X-Real-IP
-            trusted_proxies static 0.0.0.0/0
+            trusted_proxies static ${TRUSTED_PROXIES}
           }
         }

         (plane_proxy) {
           request_body {
-            max_size 5242880
+            max_size ${FILE_SIZE_LIMIT}
           }
...
         EOF
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docker-compose.image.yml` around lines 149 - 170, The Caddyfile generation in
the command block hardcodes values for trusted_proxies and max_size instead of
using the environment variables defined in the environment section. Replace the
hardcoded `0.0.0.0/0` value in the `trusted_proxies static` line with
`${TRUSTED_PROXIES}` and replace the hardcoded `5242880` value in the `max_size`
line with `${FILE_SIZE_LIMIT}` to allow these settings to be configurable at
runtime through the environment variables defined at lines 151 and 152.
.github/workflows/docker-images.yml-59-96 (1)

59-96: ⚠️ Potential issue | 🟠 Major

Pin GitHub Actions to full commit SHAs instead of version tags.

All uses: entries are tag-pinned (@v*) instead of SHA-pinned, which leaves the workflow exposed to upstream tag retargeting. Convert these to commit SHAs:

  • Line 59: actions/checkout@v6
  • Line 76: docker/setup-buildx-action@v3
  • Line 79: docker/login-action@v3
  • Line 87: docker/metadata-action@v5
  • Line 96: docker/build-push-action@v6
  • Lines 140, 143: Additional instances of docker/setup-buildx-action@v3 and docker/login-action@v3
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/docker-images.yml around lines 59 - 96, The GitHub Actions
workflow is using version tags (`@v`*) for pinning actions instead of full commit
SHAs, which exposes the workflow to upstream tag retargeting attacks. Replace
each uses entry (actions/checkout@v6, docker/setup-buildx-action@v3,
docker/login-action@v3, docker/metadata-action@v5, and
docker/build-push-action@v6) with the corresponding full commit SHA instead of
the version tag. Determine the correct commit SHA for each action version by
checking the action's releases or using a pinning tool, then update each uses
statement to reference the full commit hash in the format
owner/action@{full-commit-sha} to ensure workflow security and prevent tampering
with action definitions.

Source: Linters/SAST tools

docker-compose.prod.yml-130-130 (1)

130-130: ⚠️ Potential issue | 🟠 Major

Pin MinIO version in production compose files.

minio/minio:latest is non-deterministic and can introduce unexpected changes in behavior or security posture without an explicit code change. Pin a specific version (e.g., minio/minio:RELEASE.2024-01-31T20-20-33Z) and align the same pinning in both docker-compose.prod.yml and docker-compose.image.yml. All other infrastructure services (PostgreSQL, Redis, RabbitMQ) already use pinned versions—MinIO should match this pattern.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docker-compose.prod.yml` at line 130, The MinIO image reference uses the
non-deterministic `:latest` tag which can cause unexpected behavior changes in
production. Replace the MINIO_IMAGE default value in the docker-compose.prod.yml
file (line 130) with a specific pinned version (for example,
`minio/minio:RELEASE.2024-01-31T20-20-33Z` or another appropriate release
version). Additionally, locate and apply the same pinned version to the
MINIO_IMAGE reference in docker-compose.image.yml to ensure consistency across
all production compose files, matching the pattern already established for
PostgreSQL, Redis, and RabbitMQ services.
.github/workflows/docker-images.yml-66-70 (1)

66-70: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid direct expression interpolation in shell assignment.

Interpolating ${{ github.event.inputs.app_release }} directly into bash allows shell-expansion payloads in manually supplied input. Pass it via env: and read from $APP_RELEASE_INPUT.

Suggested fix
       - name: Prepare image variables
         id: vars
         shell: bash
+        env:
+          APP_RELEASE_INPUT: ${{ github.event.inputs.app_release }}
         run: |
           image_namespace="${GITHUB_REPOSITORY,,}"
-          app_release="${{ github.event.inputs.app_release }}"
+          app_release="$APP_RELEASE_INPUT"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/docker-images.yml around lines 66 - 70, The direct
interpolation of ${{ github.event.inputs.app_release }} in the bash script
creates a security vulnerability by allowing shell-expansion attacks through
manually supplied input. Instead, pass the input value through the `env:`
section of the GitHub Actions step as an environment variable (e.g.,
APP_RELEASE_INPUT: ${{ github.event.inputs.app_release }}) and then read from
that environment variable ($APP_RELEASE_INPUT) in the shell script assignment
rather than directly interpolating the GitHub context variable.

Source: Linters/SAST tools

apps/web/core/store/workspace-sprint.store.ts-179-183 (1)

179-183: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Scope active sprint/squad selectors to the current workspace.

Both selectors read from global maps but do not filter by the active workspace identity, so data from previously visited workspaces can leak into the current workspace lists.

Proposed fix
   get currentWorkspaceSprintIds() {
-    const workspaceSlug = this.rootStore.router.workspaceSlug;
-    if (!workspaceSlug || !this.fetchedMap[workspaceSlug]) return null;
+    const workspaceSlug = this.rootStore.router.workspaceSlug;
+    const currentWorkspaceId = this.rootStore.workspaceRoot.currentWorkspace?.id;
+    if (!workspaceSlug || !currentWorkspaceId || !this.fetchedMap[workspaceSlug]) return null;

-    const sprints = Object.values(this.sprintMap).filter((sprint) => !sprint.archived_at);
+    const sprints = Object.values(this.sprintMap).filter(
+      (sprint) => sprint.workspace_id === currentWorkspaceId && !sprint.archived_at
+    );
     return sortBy(sprints, [(sprint) => sprint.start_date ?? "", (sprint) => sprint.sort_order]).map(
       (sprint) => sprint.id
     );
   }

   get currentWorkspaceSprintAutomationIds() {
-    const workspaceSlug = this.rootStore.router.workspaceSlug;
-    if (!workspaceSlug || !this.automationFetchedMap[workspaceSlug]) return null;
+    const workspaceSlug = this.rootStore.router.workspaceSlug;
+    const currentWorkspaceId = this.rootStore.workspaceRoot.currentWorkspace?.id;
+    if (!workspaceSlug || !currentWorkspaceId || !this.automationFetchedMap[workspaceSlug]) return null;

     const automations = Object.values(this.automationMap).filter(
-      (automation) => automation.workspace_id && !automation.archived_at
+      (automation) => automation.workspace_id === currentWorkspaceId && !automation.archived_at
     );
     return sortBy(automations, [(automation) => automation.sort_order]).map((automation) => automation.id);
   }

Also applies to: 195-203

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/core/store/workspace-sprint.store.ts` around lines 179 - 183, The
sprint selectors are reading from the global sprintMap without filtering by the
currently active workspace, causing data from previously visited workspaces to
leak into current workspace views. Add a filter condition in the selector method
(starting around line 179) that checks if each sprint belongs to the active
workspace before including it in the returned list. Apply the same workspace
filtering fix to the other affected selector mentioned at lines 195-203. Access
the current workspace ID from the store's state and use it to filter the sprint
objects from this.sprintMap to ensure only sprints belonging to the active
workspace are returned.
apps/web/core/store/issue/workspace/issue.store.ts-135-137 (1)

135-137: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reset routeFilters on every fetch to avoid stale sprint filtering.

routeFilters is only updated when a truthy value is passed. After a sprint-scoped fetch, later calls without route filters can continue using the old global_sprint_id, returning incorrectly filtered results.

Proposed fix
-      if (routeFilters) this.routeFilters = routeFilters;
+      this.routeFilters = routeFilters ?? {};
       this.issueFilterStore.setRouteFilters(this.routeFilters);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/core/store/issue/workspace/issue.store.ts` around lines 135 - 137,
The routeFilters is only updated when a truthy value is passed due to the
conditional check in the code block. Remove the if statement wrapping the
assignment to this.routeFilters so that the route filters are always reset on
every fetch call, regardless of whether a truthy value is passed. This ensures
that stale filters like global_sprint_id from previous sprint-scoped fetches do
not persist and cause incorrect filtering in subsequent calls.
apps/web/core/store/workspace-sprint.store.ts-125-126 (1)

125-126: ⚠️ Potential issue | 🟠 Major

Type rootStore and sprintService explicitly.

These fields are currently untyped and fail TypeScript strict mode type-checking (strict mode is enabled via the extended config @plane/typescript-config/react-router.json). Assign types based on their runtime values: rootStore: CoreRootStore and sprintService: WorkspaceSprintService.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/core/store/workspace-sprint.store.ts` around lines 125 - 126, The
fields rootStore and sprintService in the class are currently untyped, which
violates TypeScript strict mode requirements. Add explicit type annotations to
both fields: assign the type CoreRootStore to the rootStore field and the type
WorkspaceSprintService to the sprintService field. This will ensure the code
passes strict mode type-checking as defined in the extended TypeScript
configuration.

Source: Coding guidelines

apps/web/core/components/navigation/customize-navigation-dialog.tsx-123-123 (1)

123-123: ⚠️ Potential issue | 🟠 Major

Use [...items].sort() instead of .toSorted() for ES2022 compatibility.

The TypeScript configuration for apps/web targets ES2022, which does not include Array.prototype.toSorted (ES2023). Lines 123 and 172 will fail at runtime on browsers or environments without ES2023 support. Replace with the established codebase pattern: [...items].sort((a, b) => a.sortOrder - b.sortOrder).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/core/components/navigation/customize-navigation-dialog.tsx` at line
123, The `.toSorted()` method used in the return statement is an ES2023 feature
but the TypeScript configuration targets ES2022, causing runtime failures in
incompatible environments. Replace the `.toSorted()` call with the
ES2022-compatible pattern by spreading the items array and using the `.sort()`
method instead, keeping the same comparator function. Apply this same change to
all occurrences of `.toSorted()` in the file (mentioned at lines 123 and 172).
apps/web/core/store/issue/workspace/filter.store.ts-65-65 (1)

65-65: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make routeFilters observable to prevent stale applied filter params.

routeFilters is read by getAppliedFilters()/getFilterParams() but is not registered as an observable in makeObservable. That can leave memoized filter params stale when only route filters change.

Suggested fix
 constructor(_rootStore: IIssueRootStore) {
   super();
   makeObservable(this, {
     // observables
     filters: observable,
+    routeFilters: observable,
     // computed
     issueFilters: computed,
     appliedFilters: computed,
     // fetch actions
     fetchFilters: action,
     setRouteFilters: action,
     updateFilters: action,
   });
   // root store
   this.rootIssueStore = _rootStore;
   // services
   this.issueFilterService = new WorkspaceService();
 }

 setRouteFilters = (routeFilters?: Partial<Record<string, string>>) => {
-  this.routeFilters = routeFilters ?? {};
+  this.routeFilters = { ...(routeFilters ?? {}) };
 };

Also applies to: 73-83, 101-115

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/core/store/issue/workspace/filter.store.ts` at line 65, The
`routeFilters` property is not registered as observable in the `makeObservable`
call, but it is read by the methods `getAppliedFilters()` and
`getFilterParams()`. This causes memoized filter parameters to become stale when
route filters change. To fix this, locate the `makeObservable` call in this
store class and add `routeFilters` as an observable property so that MobX
properly tracks changes to this field and invalidates dependent computed values.
apps/web/core/components/workspace/sprints/sprints-list.tsx-162-171 (1)

162-171: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Member access updates are vulnerable to lost writes.

nextMemberIds is derived from stale render state (selectedMemberIds), so quick successive toggles can overwrite each other.

Suggested direction
+ const [isUpdatingMembers, setIsUpdatingMembers] = useState(false);

  const handleMemberToggle = async (memberId: string, checked: boolean) => {
-   if (!workspaceSlugValue || !automation) return;
+   if (!workspaceSlugValue || !automation || isUpdatingMembers) return;
+   setIsUpdatingMembers(true);

-   const nextMemberIds = checked
-     ? Array.from(new Set([...selectedMemberIds, memberId]))
-     : selectedMemberIds.filter((selectedMemberId) => selectedMemberId !== memberId);
+   const latestMemberIds = getSprintSquadById(automation.id)?.member_ids ?? [];
+   const nextMemberIds = checked
+     ? Array.from(new Set([...latestMemberIds, memberId]))
+     : latestMemberIds.filter((selectedMemberId) => selectedMemberId !== memberId);

    try {
      await updateWorkspaceSprintSquadMembers(workspaceSlugValue, automation.id, nextMemberIds);
    } catch (_error) {
      setToast({
        type: TOAST_TYPE.ERROR,
        title: "Could not update sprint members",
        message: "Please try again.",
      });
+   } finally {
+     setIsUpdatingMembers(false);
    }
  };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/core/components/workspace/sprints/sprints-list.tsx` around lines 162
- 171, In the handleMemberToggle function, the nextMemberIds calculation is
based on the stale selectedMemberIds render state, which can cause lost writes
when users toggle members quickly in succession. Instead of deriving
nextMemberIds from selectedMemberIds, derive it from the current authoritative
state of members (either from the automation object's current member list or by
fetching the latest data from the API before calculating the update) to ensure
each toggle operation is based on the most current data and prevent overwriting
concurrent changes.
apps/web/core/components/workspace/sprints/sprint-modal.tsx-38-50 (1)

38-50: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate start/end ordering before submit.

The handler currently allows end_date <= start_date to be sent. Add a local guard before the API call.

Suggested fix
  const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    if (!workspaceSlug || !name.trim()) return;

+   const start = new Date(startDate);
+   const end = new Date(endDate);
+   if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime()) || end <= start) {
+     setToast({
+       type: TOAST_TYPE.ERROR,
+       title: "Could not create sprint",
+       message: "End date must be after start date.",
+     });
+     return;
+   }
+
    try {
      setIsSubmitting(true);
      await createWorkspaceSprint(workspaceSlug.toString(), {
        automation_id: automationId,
        name: name.trim(),
        description,
-       start_date: new Date(startDate).toISOString(),
-       end_date: new Date(endDate).toISOString(),
+       start_date: start.toISOString(),
+       end_date: end.toISOString(),
        timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
        logo_props: {},
      });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/core/components/workspace/sprints/sprint-modal.tsx` around lines 38
- 50, The handleSubmit function in the sprint-modal.tsx file does not validate
that the start_date comes before the end_date before making the API call to
createWorkspaceSprint. Add a validation check early in the handleSubmit function
(after checking for workspaceSlug and name) to ensure that the startDate is
earlier than endDate, and return early if this condition is not met. This guard
should prevent invalid date ranges from being sent to the API.
apps/web/core/components/archives/workspace-archives-root.tsx-73-83 (1)

73-83: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don’t swallow archived-squad fetch failures.

Line 81 currently drops fetch errors, so the UI can incorrectly show an empty state instead of a load failure.

Suggested fix
+ const [sprintsLoadError, setSprintsLoadError] = useState(false);

  useEffect(() => {
    if (!workspaceSlugValue) return;

    setIsSprintsLoading(true);
+   setSprintsLoadError(false);
    Promise.all([
      fetchWorkspaceSprintAutomations(workspaceSlugValue),
      fetchArchivedWorkspaceSprintSquads(workspaceSlugValue),
    ])
-     .catch(() => undefined)
+     .catch(() => {
+       setSprintsLoadError(true);
+       setToast({
+         type: TOAST_TYPE.ERROR,
+         title: "Could not load archived squads",
+         message: "Please try again.",
+       });
+     })
      .finally(() => setIsSprintsLoading(false));
  }, [fetchArchivedWorkspaceSprintSquads, fetchWorkspaceSprintAutomations, workspaceSlugValue]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/core/components/archives/workspace-archives-root.tsx` around lines
73 - 83, The Promise.all catch block in the useEffect hook is swallowing all
errors with .catch(() => undefined), which prevents the UI from properly
handling and displaying fetch failures for the archived squads. Modify the catch
block to allow the fetchArchivedWorkspaceSprintSquads error to propagate or be
properly handled instead of silently returning undefined, so that the UI can
detect and display a load failure state instead of showing an empty state when
the fetch fails.
apps/web/core/components/dropdowns/workspace-sprint/index.tsx-18-23 (1)

18-23: ⚠️ Potential issue | 🟠 Major

Update the callback type to properly handle async operations and prevent unhandled rejections.

The onChange callback is typed as sync-only, but downstream mutations are async. In sprint-column.tsx, handleSprint is async; in all-properties.tsx, it calls async operations without awaiting. The component must handle promise completion and errors.

Suggested fix
 type Props = {
   value: string | null;
-  onChange: (sprintId: string | null) => void;
+  onChange: (sprintId: string | null) => void | Promise<void>;
   disabled?: boolean;
   className?: string;
 };
@@
-  const dropdownOnChange = (sprintId: string | null) => {
-    onChange(sprintId);
-    handleClose();
-  };
+  const dropdownOnChange = async (sprintId: string | null) => {
+    try {
+      await onChange(sprintId);
+    } catch (error) {
+      console.error("Failed to update sprint", error);
+    } finally {
+      handleClose();
+    }
+  };

Per coding guidelines: "Use try-catch with proper error types and log errors appropriately for error handling."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/core/components/dropdowns/workspace-sprint/index.tsx` around lines
18 - 23, The onChange callback in the Props type is defined as synchronous
(returning void), but downstream implementations in sprint-column.tsx and
all-properties.tsx are asynchronous. Change the onChange property type in Props
to return a Promise to match the actual async behavior, and update any places
where onChange is called to await the result and wrap it in try-catch to handle
potential errors from the async operation.

Source: Coding guidelines

apps/web/core/components/issues/issue-layouts/properties/all-properties.tsx-177-182 (1)

177-182: ⚠️ Potential issue | 🟠 Major

handleSprint should await sprint mutations and handle errors.

At lines 180-181, async issue operations are triggered without awaiting or handling the returned promise. Failures can be silently dropped from this path.

Per coding guidelines, "Use try-catch with proper error types and log errors appropriately for error handling." The callback should be async and await both operations with proper error handling (similar to the pattern in sprint-column.tsx).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/core/components/issues/issue-layouts/properties/all-properties.tsx`
around lines 177 - 182, The handleSprint callback function is not awaiting the
async operations issueOperations.addIssueToSprint and
issueOperations.removeIssueFromSprint, allowing failures to be silently ignored.
Make the handleSprint callback async, add await keywords before both
issueOperations calls, and wrap them in a try-catch block to properly handle and
log any errors that occur during these sprint mutation operations.

Source: Coding guidelines

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

163-169: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fix non-unique week keys in month view.

At Line 169, week[0] is effectively undefined for ICalendarWeek (object map), so most rows fall back to "empty-week", producing duplicate keys and unstable React reconciliation.

Suggested fix
-Object.values(allWeeksOfActiveMonth).map((week: ICalendarWeek) => (
+Object.values(allWeeksOfActiveMonth).map((week: ICalendarWeek, weekIndex: number) => (
   <CalendarWeekDays
     selectedDate={selectedDate}
     setSelectedDate={setSelectedDate}
     handleDragAndDrop={handleDragAndDrop}
     issuesFilterStore={issuesFilterStore}
-    key={week[0]?.date.toISOString() ?? "empty-week"}
+    key={Object.values(week)[0]?.date.toISOString() ?? `empty-week-${weekIndex}`}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/core/components/issues/issue-layouts/calendar/calendar.tsx` around
lines 163 - 169, In the CalendarWeekDays mapping at line 169, the key generation
uses `week[0]?.date.toISOString()` but `week` is an object from
Object.values(allWeeksOfActiveMonth), not an array, so week[0] is undefined and
all keys default to the duplicate string "empty-week". Instead, generate a
unique key by accessing the appropriate date property directly from the
ICalendarWeek object itself, or if using Object.entries instead of
Object.values, use the week identifier from the map key, to ensure each week has
a unique and stable key for proper React reconciliation.
apps/api/plane/app/serializers/sprint.py-19-26 (1)

19-26: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate date bounds against existing instance values on partial updates.

Line 21-Line 23 only compare values present in request data. A PATCH that sends only start_date or only end_date can persist an invalid range.

Suggested fix
 class WorkspaceSprintWriteSerializer(BaseSerializer):
@@
     def validate(self, data):
+        start_date = data.get("start_date", getattr(self.instance, "start_date", None))
+        end_date = data.get("end_date", getattr(self.instance, "end_date", None))
         if (
-            data.get("start_date", None) is not None
-            and data.get("end_date", None) is not None
-            and data.get("start_date", None) > data.get("end_date", None)
+            start_date is not None
+            and end_date is not None
+            and start_date > end_date
         ):
             raise serializers.ValidationError("Start date cannot exceed end date")
         return data
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/plane/app/serializers/sprint.py` around lines 19 - 26, The validate
method in the sprint serializer only validates date bounds against values
present in the request data, which allows invalid ranges during partial updates.
When either start_date or end_date is missing from the request data (PATCH
scenario), the validation is skipped even though the missing field exists in the
database. Fix this by checking if self.instance exists (indicating a partial
update) and using the current instance values for any fields not present in the
incoming data before performing the date comparison in the validate method.
apps/api/plane/bgtasks/workspace_sprint_task.py-92-97 (1)

92-97: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Isolate per-automation failures so one bad record doesn't abort the whole run.

A failure in get(...) or process_workspace_sprint_automation(...) currently stops processing for all remaining automations in this task execution. Wrap each iteration in try/except, log with the project exception logger, and continue.

Based on learnings, background batch tasks in this repo intentionally use log-and-skip behavior to preserve full-run completion when one item fails.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/plane/bgtasks/workspace_sprint_task.py` around lines 92 - 97, Wrap
the loop body that fetches the automation object using get() and calls
process_workspace_sprint_automation(automation) in a try/except block. When an
exception occurs during either the WorkspaceSprintAutomation.objects.get() call
or the process_workspace_sprint_automation() function execution, log the
exception using the project's exception logger and then continue to the next
automation_id in the loop instead of allowing the exception to propagate and
stop all remaining processing.

Source: Learnings

apps/api/plane/bgtasks/workspace_sprint_task.py-27-29 (1)

27-29: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Guard against zero-duration automations at the data-contract level.

Line 28 divides by automation.sprint_duration_days; if this value is 0, the task crashes with ZeroDivisionError. Since the underlying field type can admit 0, enforce >= 1 in the model/serializer contract (root cause), then keep a defensive runtime check here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/plane/bgtasks/workspace_sprint_task.py` around lines 27 - 29, The
code divides by automation.sprint_duration_days on line 28 without checking if
it is zero, which will cause a ZeroDivisionError. First, add validation to the
automation model or serializer to enforce that sprint_duration_days must be
greater than or equal to 1 at the data-contract level (this is the root cause
fix). Second, add a defensive runtime check in the function containing this
division operation to verify that automation.sprint_duration_days is not zero
before performing the calculation, raising an appropriate exception or returning
a safe default value if the check fails.
packages/services/src/sprint/sprint.service.ts-16-81 (1)

16-81: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Normalize and log request errors instead of throwing possibly undefined payloads.

On Line 20 (same pattern at Lines 28, 36, 48, 56, 64, 72, 80), throw error?.response?.data can throw undefined (e.g., network failures), which breaks upstream error handling and loses stack context.

Proposed refactor
 export class WorkspaceSprintService extends APIService {
+  private rethrowApiError(error: unknown, context: string): never {
+    const apiError = error as { response?: { data?: unknown }; message?: string };
+    console.error(`[WorkspaceSprintService] ${context}`, apiError);
+    if (apiError?.response?.data instanceof Error) throw apiError.response.data;
+    if (typeof apiError?.response?.data === "string") throw new Error(apiError.response.data);
+    throw new Error(apiError?.message || `${context} failed`);
+  }
+
   async list(workspaceSlug: string): Promise<IWorkspaceSprint[]> {
-    return this.get(`/api/workspaces/${workspaceSlug}/sprints/`)
-      .then((response) => response?.data)
-      .catch((error) => {
-        throw error?.response?.data;
-      });
+    try {
+      const response = await this.get(`/api/workspaces/${workspaceSlug}/sprints/`);
+      return response?.data;
+    } catch (error) {
+      this.rethrowApiError(error, "list sprints");
+    }
   }

As per coding guidelines, "Use try-catch with proper error types and log errors appropriately for error handling."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/services/src/sprint/sprint.service.ts` around lines 16 - 81, The
error handling pattern in all methods (list, create, retrieve, update, destroy,
listIssues, addIssue, removeIssue) throws error?.response?.data which can be
undefined on network failures or when the error lacks a response structure,
breaking upstream error handling. Create a centralized error normalization
helper function or update all catch blocks to safely extract error details with
a fallback value, log the normalized error appropriately for debugging, and
throw a properly structured error object instead of possibly undefined payloads.
Ensure all eight methods consistently handle errors in this normalized way.

Source: Coding guidelines

apps/api/plane/app/views/workspace/sprint.py-327-334 (1)

327-334: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Enforce sprint visibility checks in delete path.

Line 327 deletes sprint-issue links without re-validating sprint access. A user with project write access can remove issues from private sprint automations they cannot access. Align destroy with list/create by calling _get_sprint(slug, sprint_id) before deletion.

Suggested fix
 def destroy(self, request, slug, sprint_id, issue_id):
+    self._get_sprint(slug, sprint_id)
     sprint_issue = self.get_queryset().get(issue_id=issue_id)
     issue = self._get_issue_for_write(slug, issue_id, request.user)
     if issue is None:
         return Response({"error": "Issue is not accessible"}, status=status.HTTP_403_FORBIDDEN)

     sprint_issue.delete()
     return Response(status=status.HTTP_204_NO_CONTENT)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/plane/app/views/workspace/sprint.py` around lines 327 - 334, The
destroy method is deleting sprint-issue links without validating the user's
access to the sprint itself. Call the _get_sprint method with the slug and
sprint_id parameters at the beginning of the destroy method to enforce sprint
visibility checks before proceeding with the deletion. If _get_sprint returns
None or fails the access check, return an appropriate forbidden response. This
ensures consistency with the access control patterns used in the list and create
paths, preventing users from removing issues from private sprint automations
they cannot access.
🟡 Minor comments (10)
apps/web/app/(all)/[workspaceSlug]/(projects)/squads/page.tsx-32-36 (1)

32-36: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Catch errors from initial squads/sprints fetch in the effect.

The two async calls are fired without handling rejection, which can produce unhandled promise rejections and silent UI failures.

Proposed fix
   useEffect(() => {
     if (!workspaceSlugValue) return;
-    fetchWorkspaceSprintSquads(workspaceSlugValue);
-    fetchWorkspaceSprints(workspaceSlugValue);
+    void (async () => {
+      try {
+        await Promise.all([
+          fetchWorkspaceSprintSquads(workspaceSlugValue),
+          fetchWorkspaceSprints(workspaceSlugValue),
+        ]);
+      } catch (error) {
+        console.error("Failed to load squads/sprints", error);
+      }
+    })();
   }, [fetchWorkspaceSprintSquads, fetchWorkspaceSprints, workspaceSlugValue]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/app/`(all)/[workspaceSlug]/(projects)/squads/page.tsx around lines
32 - 36, The useEffect hook containing the fetchWorkspaceSprintSquads and
fetchWorkspaceSprints function calls lacks error handling for these async
operations, which can result in unhandled promise rejections. Add error handling
to both async function calls by either chaining a .catch() handler to each
promise or wrapping the effect logic in a try-catch block within an async IIFE,
ensuring that any errors from these fetch operations are properly caught and
logged to prevent silent failures and unhandled promise rejections.
apps/web/core/components/navigation/customize-navigation-dialog.tsx-95-97 (1)

95-97: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Avoid hardcoded English strings in this preferences UI.

These lines introduce literal labels/descriptions (e.g., “Squads”, “Show limited squads on sidebar”, helper text), which bypass localization and won’t translate.

Also applies to: 380-460

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/core/components/navigation/customize-navigation-dialog.tsx` around
lines 95 - 97, The memoized callback function in the customize-navigation-dialog
component is returning a hardcoded English string "Squads" instead of using the
translation function for localization. Replace the hardcoded "Squads" string in
the ternary operator (where it checks key === "squads") with a proper
translation key lookup using the t() function, ensuring all UI labels are
properly localized throughout the preferences dialog and can be translated to
other languages.
apps/web/core/components/navigation/customize-navigation-dialog.tsx-90-91 (1)

90-91: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Keep squadCountInput synchronized with store-backed preferences.

Line 90 initializes local state only once; if sprintPreferences.limitedSquadsCount hydrates/changes later, the input can show stale data and submit the wrong limit.

Suggested fix
+useEffect(() => {
+  setSquadCountInput(sprintPreferences.limitedSquadsCount.toString());
+}, [sprintPreferences.limitedSquadsCount]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/core/components/navigation/customize-navigation-dialog.tsx` around
lines 90 - 91, The squadCountInput state is initialized once from
sprintPreferences.limitedSquadsCount but is not synchronized if the preference
changes later (such as after hydration). Add a useEffect hook that watches the
sprintPreferences.limitedSquadsCount dependency and updates the squadCountInput
state whenever the preference changes, ensuring the input always reflects the
current store-backed value and prevents submitting stale data.
apps/web/core/components/workspace/sidebar/sprints-list.tsx-192-192 (1)

192-192: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use consistent squad terminology in the empty state copy.

The section is labeled “Squads”, but the empty state says “No sprints yet”, which is inconsistent.

Suggested fix
-                <div className="px-2 py-1.5 text-12 text-tertiary">No sprints yet</div>
+                <div className="px-2 py-1.5 text-12 text-tertiary">No squads yet</div>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/core/components/workspace/sidebar/sprints-list.tsx` at line 192, The
empty state message in the sprints-list.tsx file displays "No sprints yet" but
the section is labeled "Squads", creating inconsistent terminology. Update the
text content of the empty state div (the one containing "No sprints yet") to use
"squads" terminology instead of "sprints" to match the section label and provide
consistent messaging to users.
apps/web/core/components/workspace/sprints/automation-modal.tsx-162-166 (1)

162-166: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Guard duration parsing against NaN.

Line 165 can set duration to NaN during number-input intermediate states, producing invalid API payloads.

Suggested fix
- onChange={(event) => setDuration(Math.max(1, Number(event.target.value)))}
+ onChange={(event) => {
+   const parsed = Number(event.target.value);
+   setDuration(Number.isFinite(parsed) && parsed > 0 ? parsed : 1);
+ }}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/core/components/workspace/sprints/automation-modal.tsx` around lines
162 - 166, The onChange handler for the duration input can produce NaN values
during intermediate input states (when the field is empty or being typed into),
which gets passed to the API. Modify the onChange callback that calls
setDuration to guard against NaN by checking if Number(event.target.value) is
NaN and providing a safe fallback value (such as 1 or keeping the previous
duration value) when NaN is detected.
apps/web/core/components/workspace/sidebar/sidebar-item.tsx-69-76 (1)

69-76: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Avoid hard-coding the squads label.

Line 69 bypasses i18n and will always render English for this item. Keep it translation-driven like other sidebar entries.

Suggested fix
- const label = item.key === "squads" ? "Squads" : t(item.labelTranslationKey);
+ const label = t(item.labelTranslationKey);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/core/components/workspace/sidebar/sidebar-item.tsx` around lines 69
- 76, The hardcoded "Squads" string on line 69 bypasses the i18n translation
system and will always display in English. Remove the conditional check for
item.key === "squads" in the label assignment and instead let all sidebar items,
including squads, use the t(item.labelTranslationKey) translation function
consistently. This ensures the squads label respects the application's
localization like all other sidebar entries.
packages/constants/src/issue/common.ts-218-218 (1)

218-218: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use translation keys for sprint labels to avoid untranslated UI text.

Line 218 and Line 315 use "Sprint" directly, while this config is consumed via translation-key flows. Use a key-style value (for example, common.sprint) in both places.

💡 Suggested patch
-  { key: "sprint", titleTranslationKey: "Sprint" },
+  { key: "sprint", titleTranslationKey: "common.sprint" },
   sprint: {
-    i18n_title: "Sprint",
+    i18n_title: "common.sprint",
     ascendingOrderKey: "sort_order",
     ascendingOrderTitle: "A",
     descendingOrderKey: "sort_order",

Also applies to: 315-315

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/constants/src/issue/common.ts` at line 218, The titleTranslationKey
field for the sprint configuration is using a literal string "Sprint" instead of
a proper translation key format. Replace "Sprint" with a translation key in the
format common.sprint (or similar key-style value) in both occurrences where the
sprint object is defined with the key "sprint". Ensure both instances use the
same translation key format to maintain consistency throughout the
configuration.
apps/web/core/components/dropdowns/workspace-sprint/sprint-options.tsx-121-124 (1)

121-124: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Avoid invalid div-inside-span nesting in option rows.

At Line 123, option.content is a <div> wrapped in a <span>, which is invalid HTML and may cause rendering glitches. Use a block wrapper (div) instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/core/components/dropdowns/workspace-sprint/sprint-options.tsx`
around lines 121 - 124, The `<span>` element wrapping `{option.content}` at line
123 creates invalid HTML nesting since `option.content` is a `<div>` (block
element) and cannot be nested inside a `<span>` (inline element). Replace the
outer `<span>` wrapper with a `<div>` element while keeping the same className
and structure to maintain the layout and flex properties. This will resolve the
nesting conflict and prevent potential rendering issues.
apps/web/core/components/dropdowns/workspace-sprint/sprint-options.tsx-47-52 (1)

47-52: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Catch fetchWorkspaceSprints errors in the open effect.

At Line 50, a rejected fetch promise is not handled. This leaves failure paths as unhandled rejections when sprint loading fails.

As per coding guidelines, "Use try-catch with proper error types and log errors appropriately for error handling".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/core/components/dropdowns/workspace-sprint/sprint-options.tsx`
around lines 47 - 52, The `fetchWorkspaceSprints(slug)` call in the useEffect is
not handling potential errors from the rejected promise. Wrap the call to
`fetchWorkspaceSprints(slug)` in a try-catch block to properly handle errors and
log them appropriately. Since useEffect cannot be async directly, create an
async IIFE inside the effect to properly await the function call and catch any
errors that may occur during sprint fetching, ensuring all error paths are
handled.

Source: Coding guidelines

apps/api/plane/app/serializers/sprint.py-34-38 (1)

34-38: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Allow clearing automation_id explicitly during updates.

Line 36 only applies updates when automation_id is truthy, so a client cannot clear an existing relation by sending null.

Suggested fix
     def update(self, instance, validated_data):
-        automation_id = validated_data.pop("automation_id", None)
-        if automation_id:
-            validated_data["automation_id"] = automation_id
+        sentinel = object()
+        automation_id = validated_data.pop("automation_id", sentinel)
+        if automation_id is not sentinel:
+            validated_data["automation_id"] = automation_id
         return super().update(instance, validated_data)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/plane/app/serializers/sprint.py` around lines 34 - 38, The update
method in the SprintSerializer is preventing clients from clearing the
automation_id field by setting it to null. The issue is in the update method
where automation_id is popped from validated_data and only re-added if it is
truthy, which excludes None values. Instead of checking if automation_id is
truthy, you need to check whether the automation_id key was actually provided in
the validated_data before popping it. If the key exists (regardless of whether
the value is None or a truthy value), it should be re-added to validated_data so
the parent update method can process the explicit clear operation. Modify the
conditional logic to distinguish between automation_id not being provided versus
being explicitly set to null.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 351155bf-7af6-4766-9413-3fec0a91b193

📥 Commits

Reviewing files that changed from the base of the PR and between 4a0746b and 6db63a5.

📒 Files selected for processing (102)
  • .cursor/rules/pr-deploy-publicacao.mdc
  • .github/workflows/docker-images.yml
  • .gitignore
  • apps/api/plane/app/serializers/__init__.py
  • apps/api/plane/app/serializers/issue.py
  • apps/api/plane/app/serializers/sprint.py
  • apps/api/plane/app/serializers/view.py
  • apps/api/plane/app/urls/workspace.py
  • apps/api/plane/app/views/__init__.py
  • apps/api/plane/app/views/issue/base.py
  • apps/api/plane/app/views/view/base.py
  • apps/api/plane/app/views/workspace/sprint.py
  • apps/api/plane/bgtasks/workspace_sprint_task.py
  • apps/api/plane/celery.py
  • apps/api/plane/db/migrations/0122_workspace_sprint.py
  • apps/api/plane/db/migrations/0123_workspace_sprint_automation.py
  • apps/api/plane/db/migrations/0124_alter_workspacesprint_created_by_and_more.py
  • apps/api/plane/db/migrations/0125_workspaceuserproperties_navigation_sprint_preference.py
  • apps/api/plane/db/migrations/0126_workspace_sprint_automation_access.py
  • apps/api/plane/db/migrations/0127_workspacesprintautomation_logo_props.py
  • apps/api/plane/db/migrations/0128_workspacesprintautomation_archived_at.py
  • apps/api/plane/db/migrations/0129_alter_workspacesprintautomationmember_created_by_and_more.py
  • apps/api/plane/db/migrations/0130_alter_workspacesprintautomation_options_and_more.py
  • apps/api/plane/db/migrations/0131_workspaceuserproperties_navigation_squad_limit.py
  • apps/api/plane/db/models/__init__.py
  • apps/api/plane/db/models/sprint.py
  • apps/api/plane/db/models/workspace.py
  • apps/api/plane/tests/contract/app/test_workspace_sprints.py
  • apps/api/plane/utils/filters/converters.py
  • apps/api/plane/utils/filters/filterset.py
  • apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/archives/page.tsx
  • apps/web/app/(all)/[workspaceSlug]/(projects)/sidebar.tsx
  • apps/web/app/(all)/[workspaceSlug]/(projects)/sprints/[sprintId]/page.tsx
  • apps/web/app/(all)/[workspaceSlug]/(projects)/sprints/header.tsx
  • apps/web/app/(all)/[workspaceSlug]/(projects)/sprints/layout.tsx
  • apps/web/app/(all)/[workspaceSlug]/(projects)/sprints/page.tsx
  • apps/web/app/(all)/[workspaceSlug]/(projects)/sprints/work-items/[workspaceSprintId]/page.tsx
  • apps/web/app/(all)/[workspaceSlug]/(projects)/squads/layout.tsx
  • apps/web/app/(all)/[workspaceSlug]/(projects)/squads/page.tsx
  • apps/web/app/routes/core.ts
  • apps/web/ce/components/issues/issue-layouts/utils.tsx
  • apps/web/ce/components/workspace/sidebar/extended-sidebar-item.tsx
  • apps/web/ce/components/workspace/sidebar/helper.tsx
  • apps/web/core/components/archives/index.ts
  • apps/web/core/components/archives/workspace-archives-root.tsx
  • apps/web/core/components/dropdowns/workspace-sprint/index.tsx
  • apps/web/core/components/dropdowns/workspace-sprint/sprint-options.tsx
  • apps/web/core/components/issues/issue-layouts/calendar/base-calendar-root.tsx
  • apps/web/core/components/issues/issue-layouts/calendar/calendar.tsx
  • apps/web/core/components/issues/issue-layouts/calendar/day-tile.tsx
  • apps/web/core/components/issues/issue-layouts/calendar/dropdowns/months-dropdown.tsx
  • apps/web/core/components/issues/issue-layouts/calendar/dropdowns/options-dropdown.tsx
  • apps/web/core/components/issues/issue-layouts/calendar/header.tsx
  • apps/web/core/components/issues/issue-layouts/calendar/issue-block.tsx
  • apps/web/core/components/issues/issue-layouts/calendar/week-days.tsx
  • apps/web/core/components/issues/issue-layouts/filters/header/display-filters/display-properties.tsx
  • apps/web/core/components/issues/issue-layouts/gantt/base-gantt-root.tsx
  • apps/web/core/components/issues/issue-layouts/kanban/base-kanban-root.tsx
  • apps/web/core/components/issues/issue-layouts/list/base-list-root.tsx
  • apps/web/core/components/issues/issue-layouts/properties/all-properties.tsx
  • apps/web/core/components/issues/issue-layouts/roots/all-issue-layout-root.tsx
  • apps/web/core/components/issues/issue-layouts/spreadsheet/columns/index.ts
  • apps/web/core/components/issues/issue-layouts/spreadsheet/columns/project-column.tsx
  • apps/web/core/components/issues/issue-layouts/spreadsheet/columns/sprint-column.tsx
  • apps/web/core/components/issues/issue-layouts/spreadsheet/issue-row.tsx
  • apps/web/core/components/issues/issue-layouts/spreadsheet/roots/workspace-root.tsx
  • apps/web/core/components/issues/issue-layouts/spreadsheet/spreadsheet-table.tsx
  • apps/web/core/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx
  • apps/web/core/components/navigation/customize-navigation-dialog.tsx
  • apps/web/core/components/views/helper.tsx
  • apps/web/core/components/workspace/sidebar/sidebar-item.tsx
  • apps/web/core/components/workspace/sidebar/sprints-list.tsx
  • apps/web/core/components/workspace/sprints/automation-modal.tsx
  • apps/web/core/components/workspace/sprints/sprint-modal.tsx
  • apps/web/core/components/workspace/sprints/sprints-list.tsx
  • apps/web/core/hooks/store/use-workspace-sprint.ts
  • apps/web/core/hooks/use-group-dragndrop.ts
  • apps/web/core/hooks/use-issues-actions.tsx
  • apps/web/core/hooks/use-navigation-preferences.ts
  • apps/web/core/services/sprint.service.ts
  • apps/web/core/store/issue/helpers/issue-filter-helper.store.ts
  • apps/web/core/store/issue/issue-details/issue.store.ts
  • apps/web/core/store/issue/workspace/filter.store.ts
  • apps/web/core/store/issue/workspace/issue.store.ts
  • apps/web/core/store/pages/project-page.store.ts
  • apps/web/core/store/root.store.ts
  • apps/web/core/store/workspace-sprint.store.ts
  • docker-compose.image.yml
  • docker-compose.prod.yml
  • docs/production-docker.md
  • packages/constants/src/issue/common.ts
  • packages/constants/src/workspace.ts
  • packages/services/src/index.ts
  • packages/services/src/sprint/index.ts
  • packages/services/src/sprint/sprint.service.ts
  • packages/types/src/index.ts
  • packages/types/src/issues/issue.ts
  • packages/types/src/navigation-preferences.ts
  • packages/types/src/sprint/index.ts
  • packages/types/src/sprint/sprint.ts
  • packages/types/src/view-props.ts
  • packages/utils/src/work-item/base.ts

Comment on lines +764 to +765
global_sprint_id = serializers.PrimaryKeyRelatedField(read_only=True)
global_sprint_name = serializers.CharField(read_only=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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify serializer field type and annotation source shape.
rg -nP 'global_sprint_id\s*=\s*serializers\.PrimaryKeyRelatedField' apps/api/plane/app/serializers/issue.py
rg -nP 'global_sprint_id\s*=\s*Subquery\(' apps/api/plane/app/views/issue/base.py
rg -nP 'values\(\s*"sprint_id"\s*\)' apps/api/plane/app/views/issue/base.py

Repository: makeplane/plane

Length of output: 231


🏁 Script executed:

# Get context around the Subquery definitions at lines 119 and 239 in base.py
sed -n '115,130p' apps/api/plane/app/views/issue/base.py
sed -n '235,250p' apps/api/plane/app/views/issue/base.py

# Check what's at lines 793-794 in the serializer
sed -n '791,796p' apps/api/plane/app/serializers/issue.py

Repository: makeplane/plane

Length of output: 1587


🏁 Script executed:

# Get clearer view of lines 793-796 in serializer
sed -n '790,800p' apps/api/plane/app/serializers/issue.py

Repository: makeplane/plane

Length of output: 363


🏁 Script executed:

# Search for all definitions of global_sprint_id and global_sprint_name field definitions
rg -n 'global_sprint_id\s*=\s*serializers\.' apps/api/plane/app/serializers/issue.py
rg -n 'global_sprint_name\s*=\s*serializers\.' apps/api/plane/app/serializers/issue.py

Repository: makeplane/plane

Length of output: 204


🏁 Script executed:

# Find all class definitions in the serializer file and their line numbers
rg -n '^class ' apps/api/plane/app/serializers/issue.py | head -20

# Show context around line 793
sed -n '780,810p' apps/api/plane/app/serializers/issue.py

Repository: makeplane/plane

Length of output: 1864


🏁 Script executed:

# Get more context to see which class the fields tuple belongs to and check around 793-794
sed -n '750,815p' apps/api/plane/app/serializers/issue.py

Repository: makeplane/plane

Length of output: 2142


🏁 Script executed:

# Search for all classes and look for ones that might have global_sprint_id definitions
rg -n '^class ' apps/api/plane/app/serializers/issue.py | tail -20

# Get file total line count
wc -l apps/api/plane/app/serializers/issue.py

# Search for all global_sprint_id field definitions (not in fields lists)
rg -B2 'global_sprint_id\s*=\s*serializers\.' apps/api/plane/app/serializers/issue.py

Repository: makeplane/plane

Length of output: 1336


🏁 Script executed:

# Check lines 793-794 more carefully with line numbers
sed -n '791,800p' apps/api/plane/app/serializers/issue.py | cat -n

# Check if there are other serializer classes that might have global_sprint_id fields
rg -A5 'class.*Serializer.*:' apps/api/plane/app/serializers/issue.py | grep -A5 'global_sprint_id'

# Double check the exact location - show lines 819 onwards to see IssueListDetailSerializer
sed -n '819,860p' apps/api/plane/app/serializers/issue.py

Repository: makeplane/plane

Length of output: 2591


🏁 Script executed:

# Check if IssueDetailSerializer also has global_sprint_id definition (it inherits from IssueSerializer)
sed -n '931,945p' apps/api/plane/app/serializers/issue.py

# Check IssueListDetailSerializer more - does it define global_sprint_id as a field?
sed -n '819,925p' apps/api/plane/app/serializers/issue.py | grep -n 'global_sprint_id'

# Search for any other global_sprint_id definitions
rg -n 'global_sprint_id.*=' apps/api/plane/app/serializers/issue.py

Repository: makeplane/plane

Length of output: 693


🏁 Script executed:

# Check exact lines 793-794
sed -n '793,794p' apps/api/plane/app/serializers/issue.py

# Also check context around those lines
sed -n '790,800p' apps/api/plane/app/serializers/issue.py | cat -n

Repository: makeplane/plane

Length of output: 506


🏁 Script executed:

# Let's find all field definitions with their line numbers for global_sprint_id
rg -n 'global_sprint_id\s*=' apps/api/plane/app/serializers/issue.py

# Check if IssueLiteSerializer or IssueFlatSerializer have these fields
sed -n '52,70p' apps/api/plane/app/serializers/issue.py
sed -n '924,930p' apps/api/plane/app/serializers/issue.py

Repository: makeplane/plane

Length of output: 715


🏁 Script executed:

# Let's understand what type sprint_id is in the model
rg -n 'class WorkspaceSprintIssue' apps/api/plane/app/models/

# Find the model file
fd 'models\.py' apps/api/plane/app/models/ | head -5

# Check what sprint_id field type is
rg -B5 -A5 'sprint_id.*=' apps/api/plane/app/models/

Repository: makeplane/plane

Length of output: 307


🏁 Script executed:

# Find where models are located
fd 'models' apps/api/plane/app/ -type d

# Search for WorkspaceSprintIssue model
rg -l 'class WorkspaceSprintIssue' apps/api/

Repository: makeplane/plane

Length of output: 369


🏁 Script executed:

# Check the WorkspaceSprintIssue model definition
rg -B5 -A15 'class WorkspaceSprintIssue' apps/api/plane/db/models/sprint.py

# Also check if sprint_id is a UUID field
rg -n 'sprint_id' apps/api/plane/db/models/sprint.py | head -10

Repository: makeplane/plane

Length of output: 1016


🏁 Script executed:

# Check the WorkspaceSprint model to confirm sprint_id is UUID
rg -B2 -A10 'class WorkspaceSprint' apps/api/plane/db/models/sprint.py | head -20

# Verify the annotation is truly providing scalar values by checking the actual queryset call
sed -n '115,130p' apps/api/plane/app/views/issue/base.py
sed -n '235,250p' apps/api/plane/app/views/issue/base.py

Repository: makeplane/plane

Length of output: 2155


🏁 Script executed:

# Verify WorkspaceSprint model has id as UUID by checking the parent class
rg -B5 'class WorkspaceBaseModel' apps/api/plane/db/models/

# Check what type of ID field WorkspaceBaseModel uses
rg -A20 'class WorkspaceBaseModel' apps/api/plane/db/models/base.py

Repository: makeplane/plane

Length of output: 437


🏁 Script executed:

# Check the BaseModel class to understand the ID field type
rg -A30 'class BaseModel' apps/api/plane/db/models/base.py | head -40

Repository: makeplane/plane

Length of output: 1242


Use UUIDField for annotated global_sprint_id value.

Line 764 defines global_sprint_id as PrimaryKeyRelatedField, but the queryset annotation in apps/api/plane/app/views/issue/base.py provides a scalar UUID via values("sprint_id") within a Subquery, not a related model instance. This type mismatch can cause serialization errors on issue list responses.

Suggested fix
-    global_sprint_id = serializers.PrimaryKeyRelatedField(read_only=True)
+    global_sprint_id = serializers.UUIDField(read_only=True, allow_null=True)
📝 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
global_sprint_id = serializers.PrimaryKeyRelatedField(read_only=True)
global_sprint_name = serializers.CharField(read_only=True)
global_sprint_id = serializers.UUIDField(read_only=True, allow_null=True)
global_sprint_name = serializers.CharField(read_only=True)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/plane/app/serializers/issue.py` around lines 764 - 765, The
global_sprint_id field in the IssueSerializer is incorrectly defined as a
PrimaryKeyRelatedField, but the queryset annotation provides a scalar UUID value
rather than a related model instance. Change the global_sprint_id field
definition from PrimaryKeyRelatedField to UUIDField with read_only=True to match
the actual data type being provided by the annotated queryset. This will resolve
the serialization type mismatch that causes errors in issue list responses.

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