[PLAN-90] fix: persist page list sort preference after reload#9282
[PLAN-90] fix: persist page list sort preference after reload#9282alanssant0s wants to merge 19 commits into
Conversation
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
|
|
|
Important Review skippedToo many files! This PR contains 153 files, which is 3 over the limit of 150. To get a review, narrow the scope: Upgrade to a paid plan to raise the limit. ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (153)
You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughThis 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. ChangesWorkspace sprints and squads
Deployment and publishing automation
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
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 3❌ Failed checks (3 warnings)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
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>
6db63a5 to
16076c5
Compare
There was a problem hiding this comment.
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 winDo 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 winScope
packages: writeto publishing jobs only.
packages: writeat workflow scope grants write-capable token permissions to every job, includingdeploy-dokploy, which does not publish images. Move package-write permission tobuild-and-pushandpromote-mainonly.🤖 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 winAdd 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_PROXIESandFILE_SIZE_LIMITare currently ignored.The generated Caddyfile hardcodes
trusted_proxies static 0.0.0.0/0andmax_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 | 🟠 MajorPin 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@v3anddocker/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 | 🟠 MajorPin MinIO version in production compose files.
minio/minio:latestis 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 bothdocker-compose.prod.ymlanddocker-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 winAvoid 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 viaenv: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 winScope 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 winReset
routeFilterson every fetch to avoid stale sprint filtering.
routeFiltersis only updated when a truthy value is passed. After a sprint-scoped fetch, later calls without route filters can continue using the oldglobal_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 | 🟠 MajorType
rootStoreandsprintServiceexplicitly.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: CoreRootStoreandsprintService: 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 | 🟠 MajorUse
[...items].sort()instead of.toSorted()for ES2022 compatibility.The TypeScript configuration for
apps/webtargets ES2022, which does not includeArray.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 winMake
routeFiltersobservable to prevent stale applied filter params.
routeFiltersis read bygetAppliedFilters()/getFilterParams()but is not registered as an observable inmakeObservable. 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 winMember access updates are vulnerable to lost writes.
nextMemberIdsis 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 winValidate start/end ordering before submit.
The handler currently allows
end_date <= start_dateto 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 winDon’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 | 🟠 MajorUpdate the callback type to properly handle async operations and prevent unhandled rejections.
The
onChangecallback is typed as sync-only, but downstream mutations are async. Insprint-column.tsx,handleSprintis async; inall-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
handleSprintshould 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 winFix non-unique week keys in month view.
At Line 169,
week[0]is effectively undefined forICalendarWeek(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 winValidate 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_dateor onlyend_datecan 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 winIsolate per-automation failures so one bad record doesn't abort the whole run.
A failure in
get(...)orprocess_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 liftGuard against zero-duration automations at the data-contract level.
Line 28 divides by
automation.sprint_duration_days; if this value is0, the task crashes withZeroDivisionError. Since the underlying field type can admit0, enforce>= 1in 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 winNormalize and log request errors instead of throwing possibly
undefinedpayloads.On Line 20 (same pattern at Lines 28, 36, 48, 56, 64, 72, 80),
throw error?.response?.datacan throwundefined(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 winEnforce 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
destroywithlist/createby 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 winCatch 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 winAvoid 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 winKeep
squadCountInputsynchronized with store-backed preferences.Line 90 initializes local state only once; if
sprintPreferences.limitedSquadsCounthydrates/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 winUse 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 winGuard duration parsing against
NaN.Line 165 can set
durationtoNaNduring 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 winAvoid 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 winUse 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 winAvoid invalid
div-inside-spannesting in option rows.At Line 123,
option.contentis 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 winCatch
fetchWorkspaceSprintserrors 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 winAllow clearing
automation_idexplicitly during updates.Line 36 only applies updates when
automation_idis truthy, so a client cannot clear an existing relation by sendingnull.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
📒 Files selected for processing (102)
.cursor/rules/pr-deploy-publicacao.mdc.github/workflows/docker-images.yml.gitignoreapps/api/plane/app/serializers/__init__.pyapps/api/plane/app/serializers/issue.pyapps/api/plane/app/serializers/sprint.pyapps/api/plane/app/serializers/view.pyapps/api/plane/app/urls/workspace.pyapps/api/plane/app/views/__init__.pyapps/api/plane/app/views/issue/base.pyapps/api/plane/app/views/view/base.pyapps/api/plane/app/views/workspace/sprint.pyapps/api/plane/bgtasks/workspace_sprint_task.pyapps/api/plane/celery.pyapps/api/plane/db/migrations/0122_workspace_sprint.pyapps/api/plane/db/migrations/0123_workspace_sprint_automation.pyapps/api/plane/db/migrations/0124_alter_workspacesprint_created_by_and_more.pyapps/api/plane/db/migrations/0125_workspaceuserproperties_navigation_sprint_preference.pyapps/api/plane/db/migrations/0126_workspace_sprint_automation_access.pyapps/api/plane/db/migrations/0127_workspacesprintautomation_logo_props.pyapps/api/plane/db/migrations/0128_workspacesprintautomation_archived_at.pyapps/api/plane/db/migrations/0129_alter_workspacesprintautomationmember_created_by_and_more.pyapps/api/plane/db/migrations/0130_alter_workspacesprintautomation_options_and_more.pyapps/api/plane/db/migrations/0131_workspaceuserproperties_navigation_squad_limit.pyapps/api/plane/db/models/__init__.pyapps/api/plane/db/models/sprint.pyapps/api/plane/db/models/workspace.pyapps/api/plane/tests/contract/app/test_workspace_sprints.pyapps/api/plane/utils/filters/converters.pyapps/api/plane/utils/filters/filterset.pyapps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/archives/page.tsxapps/web/app/(all)/[workspaceSlug]/(projects)/sidebar.tsxapps/web/app/(all)/[workspaceSlug]/(projects)/sprints/[sprintId]/page.tsxapps/web/app/(all)/[workspaceSlug]/(projects)/sprints/header.tsxapps/web/app/(all)/[workspaceSlug]/(projects)/sprints/layout.tsxapps/web/app/(all)/[workspaceSlug]/(projects)/sprints/page.tsxapps/web/app/(all)/[workspaceSlug]/(projects)/sprints/work-items/[workspaceSprintId]/page.tsxapps/web/app/(all)/[workspaceSlug]/(projects)/squads/layout.tsxapps/web/app/(all)/[workspaceSlug]/(projects)/squads/page.tsxapps/web/app/routes/core.tsapps/web/ce/components/issues/issue-layouts/utils.tsxapps/web/ce/components/workspace/sidebar/extended-sidebar-item.tsxapps/web/ce/components/workspace/sidebar/helper.tsxapps/web/core/components/archives/index.tsapps/web/core/components/archives/workspace-archives-root.tsxapps/web/core/components/dropdowns/workspace-sprint/index.tsxapps/web/core/components/dropdowns/workspace-sprint/sprint-options.tsxapps/web/core/components/issues/issue-layouts/calendar/base-calendar-root.tsxapps/web/core/components/issues/issue-layouts/calendar/calendar.tsxapps/web/core/components/issues/issue-layouts/calendar/day-tile.tsxapps/web/core/components/issues/issue-layouts/calendar/dropdowns/months-dropdown.tsxapps/web/core/components/issues/issue-layouts/calendar/dropdowns/options-dropdown.tsxapps/web/core/components/issues/issue-layouts/calendar/header.tsxapps/web/core/components/issues/issue-layouts/calendar/issue-block.tsxapps/web/core/components/issues/issue-layouts/calendar/week-days.tsxapps/web/core/components/issues/issue-layouts/filters/header/display-filters/display-properties.tsxapps/web/core/components/issues/issue-layouts/gantt/base-gantt-root.tsxapps/web/core/components/issues/issue-layouts/kanban/base-kanban-root.tsxapps/web/core/components/issues/issue-layouts/list/base-list-root.tsxapps/web/core/components/issues/issue-layouts/properties/all-properties.tsxapps/web/core/components/issues/issue-layouts/roots/all-issue-layout-root.tsxapps/web/core/components/issues/issue-layouts/spreadsheet/columns/index.tsapps/web/core/components/issues/issue-layouts/spreadsheet/columns/project-column.tsxapps/web/core/components/issues/issue-layouts/spreadsheet/columns/sprint-column.tsxapps/web/core/components/issues/issue-layouts/spreadsheet/issue-row.tsxapps/web/core/components/issues/issue-layouts/spreadsheet/roots/workspace-root.tsxapps/web/core/components/issues/issue-layouts/spreadsheet/spreadsheet-table.tsxapps/web/core/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsxapps/web/core/components/navigation/customize-navigation-dialog.tsxapps/web/core/components/views/helper.tsxapps/web/core/components/workspace/sidebar/sidebar-item.tsxapps/web/core/components/workspace/sidebar/sprints-list.tsxapps/web/core/components/workspace/sprints/automation-modal.tsxapps/web/core/components/workspace/sprints/sprint-modal.tsxapps/web/core/components/workspace/sprints/sprints-list.tsxapps/web/core/hooks/store/use-workspace-sprint.tsapps/web/core/hooks/use-group-dragndrop.tsapps/web/core/hooks/use-issues-actions.tsxapps/web/core/hooks/use-navigation-preferences.tsapps/web/core/services/sprint.service.tsapps/web/core/store/issue/helpers/issue-filter-helper.store.tsapps/web/core/store/issue/issue-details/issue.store.tsapps/web/core/store/issue/workspace/filter.store.tsapps/web/core/store/issue/workspace/issue.store.tsapps/web/core/store/pages/project-page.store.tsapps/web/core/store/root.store.tsapps/web/core/store/workspace-sprint.store.tsdocker-compose.image.ymldocker-compose.prod.ymldocs/production-docker.mdpackages/constants/src/issue/common.tspackages/constants/src/workspace.tspackages/services/src/index.tspackages/services/src/sprint/index.tspackages/services/src/sprint/sprint.service.tspackages/types/src/index.tspackages/types/src/issues/issue.tspackages/types/src/navigation-preferences.tspackages/types/src/sprint/index.tspackages/types/src/sprint/sprint.tspackages/types/src/view-props.tspackages/utils/src/work-item/base.ts
| global_sprint_id = serializers.PrimaryKeyRelatedField(read_only=True) | ||
| global_sprint_name = serializers.CharField(read_only=True) |
There was a problem hiding this comment.
🧩 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.pyRepository: 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.pyRepository: 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.pyRepository: 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.pyRepository: 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.pyRepository: 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.pyRepository: 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.pyRepository: 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.pyRepository: 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.pyRepository: 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 -nRepository: 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.pyRepository: 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 -10Repository: 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.pyRepository: 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.pyRepository: 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 -40Repository: 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.
| 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.
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
sortKeyesortBynolocalStoragepor projeto (page_display_filters), seguindo o mesmo padrão doModuleFilterStore. A preferência é restaurada ao abrir o projeto e atualizada sempre que o usuário altera a ordenação.Type of Change
Screenshots and Media (if applicable)
Test Scenarios
References
Claude Code session
Made with Cursor
Summary by CodeRabbit
Release Notes
New Features
Documentation