Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions apps/api/plane/app/serializers/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

# Module imports
from .base import BaseSerializer, DynamicBaseSerializer
from django.db.models import Max
Comment on lines 3 to +6
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

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

The Django import should be grouped with other Django/third-party imports at the top. Currently, django.db.models.Max is placed between local module imports. Consider moving it to line 3, after the rest_framework import, to follow Python import conventions (standard library, third-party, local modules).

Suggested change
# Module imports
from .base import BaseSerializer, DynamicBaseSerializer
from django.db.models import Max
from django.db.models import Max
# Module imports
from .base import BaseSerializer, DynamicBaseSerializer

Copilot uses AI. Check for mistakes.
from plane.app.serializers.workspace import WorkspaceLiteSerializer
from plane.app.serializers.user import UserLiteSerializer, UserAdminLiteSerializer
from plane.db.models import (
Expand All @@ -12,6 +13,7 @@
ProjectIdentifier,
DeployBoard,
ProjectPublicMember,
IssueSequence
)
from plane.utils.content_validator import (
validate_html_content,
Expand Down Expand Up @@ -105,6 +107,7 @@ class ProjectListSerializer(DynamicBaseSerializer):
members = serializers.SerializerMethodField()
cover_image_url = serializers.CharField(read_only=True)
inbox_view = serializers.BooleanField(read_only=True, source="intake_view")
next_work_item_sequence = serializers.SerializerMethodField()

def get_members(self, obj):
project_members = getattr(obj, "members_list", None)
Expand All @@ -113,6 +116,11 @@ def get_members(self, obj):
return [member.member_id for member in project_members if member.is_active and not member.member.is_bot]
return []

def get_next_work_item_sequence(self, obj):
"""Get the next sequence ID that will be assigned to a new issue"""
max_sequence = IssueSequence.objects.filter(project_id=obj.id).aggregate(max_seq=Max("sequence"))["max_seq"]
return (max_sequence + 1) if max_sequence else 1
Comment on lines +119 to +122
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

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

This method performs a database query for each project being serialized, which will cause N+1 query problems when listing multiple projects. When serializing a list of projects, this will execute one additional query per project to calculate the max sequence.

Consider using an annotation in the view's queryset to pre-calculate this value, similar to how other fields like 'is_favorite', 'member_role', and 'sort_order' are annotated. This would execute the aggregation in a single query using a subquery.

Copilot uses AI. Check for mistakes.

class Meta:
model = Project
fields = "__all__"
Expand Down
11 changes: 8 additions & 3 deletions apps/web/core/components/issues/issue-layouts/list/block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { IssueIdentifier } from "@/plane-web/components/issues/issue-details/iss
import { IssueStats } from "@/plane-web/components/issues/issue-layouts/issue-stats";
// types
import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC";
import { calculateIdentifierWidth } from "../utils";
import type { TRenderQuickActions } from "./list-view-types";

interface IssueBlockProps {
Expand Down Expand Up @@ -76,7 +77,7 @@ export const IssueBlock = observer(function IssueBlock(props: IssueBlockProps) {
const projectId = routerProjectId?.toString();
// hooks
const { sidebarCollapsed: isSidebarCollapsed } = useAppTheme();
const { getProjectIdentifierById } = useProject();
const { getProjectIdentifierById, currentProjectNextSequenceId } = useProject();
const {
getIsIssuePeeked,
peekIssue,
Expand Down Expand Up @@ -150,8 +151,12 @@ export const IssueBlock = observer(function IssueBlock(props: IssueBlockProps) {
}
};

//TODO: add better logic. This is to have a min width for ID/Key based on the length of project identifier
const keyMinWidth = displayProperties?.key ? (projectIdentifier?.length ?? 0) * 7 : 0;
// Calculate width for: projectIdentifier + "-" + dynamic sequence number digits
// Use next_work_item_sequence from backend (static value from project endpoint)
const maxSequenceId = currentProjectNextSequenceId ?? 1;
const keyMinWidth = displayProperties?.key
? calculateIdentifierWidth(projectIdentifier?.length ?? 0, maxSequenceId)
: 0;

const workItemLink = generateWorkItemLink({
workspaceSlug,
Expand Down
15 changes: 15 additions & 0 deletions apps/web/core/components/issues/issue-layouts/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -748,3 +748,18 @@ export const isFiltersApplied = (filters: IIssueFilterOptions): boolean =>
if (Array.isArray(value)) return value.length > 0;
return value !== undefined && value !== null && value !== "";
});

/**
* Calculates the minimum width needed for issue identifiers in list layouts
* @param projectIdentifierLength - Length of the project identifier (e.g., "PROJ" = 4)
* @param maxSequenceId - Maximum sequence ID in the project (e.g., 1234)
* @returns Width in pixels needed to display the identifier
*
* @example
* // For "PROJ-1234"
* calculateIdentifierWidth(4, 1234) // Returns width for "PROJ" + "-" + "1234"
*/
export const calculateIdentifierWidth = (projectIdentifierLength: number, maxSequenceId: number): number => {
const sequenceDigits = Math.max(1, Math.floor(Math.log10(maxSequenceId)) + 1);
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

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

The calculation will fail for maxSequenceId = 0. Math.log10(0) returns -Infinity, which when floored and added to 1 results in -Infinity, and Math.max(1, -Infinity) returns 1. While this technically works due to Math.max, it's inefficient and unclear.

Consider adding an explicit check: if maxSequenceId is 0 or negative, return a sensible default width or treat it the same as maxSequenceId = 1, as there's no meaningful difference in width calculation between these cases.

Suggested change
const sequenceDigits = Math.max(1, Math.floor(Math.log10(maxSequenceId)) + 1);
// Ensure maxSequenceId is at least 1 to avoid Math.log10(0) or negative numbers
const safeSequenceId = maxSequenceId > 0 ? maxSequenceId : 1;
const sequenceDigits = Math.floor(Math.log10(safeSequenceId)) + 1;

Copilot uses AI. Check for mistakes.
return projectIdentifierLength * 7 + 7 + sequenceDigits * 7; // project identifier chars + dash + sequence digits
};
Comment on lines +752 to +765
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Clamp maxSequenceId to avoid NaN widths.
If maxSequenceId is ever non-finite, sequenceDigits becomes NaN and the function returns NaN, which can break inline styles.

 export const calculateIdentifierWidth = (projectIdentifierLength: number, maxSequenceId: number): number => {
-  const sequenceDigits = Math.max(1, Math.floor(Math.log10(maxSequenceId)) + 1);
+  const safeMax = Number.isFinite(maxSequenceId) && maxSequenceId > 0 ? Math.floor(maxSequenceId) : 1;
+  const sequenceDigits = Math.max(1, Math.floor(Math.log10(safeMax)) + 1);
   return projectIdentifierLength * 7 + 7 + sequenceDigits * 7; // project identifier chars + dash + sequence digits
 };
📝 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
/**
* Calculates the minimum width needed for issue identifiers in list layouts
* @param projectIdentifierLength - Length of the project identifier (e.g., "PROJ" = 4)
* @param maxSequenceId - Maximum sequence ID in the project (e.g., 1234)
* @returns Width in pixels needed to display the identifier
*
* @example
* // For "PROJ-1234"
* calculateIdentifierWidth(4, 1234) // Returns width for "PROJ" + "-" + "1234"
*/
export const calculateIdentifierWidth = (projectIdentifierLength: number, maxSequenceId: number): number => {
const sequenceDigits = Math.max(1, Math.floor(Math.log10(maxSequenceId)) + 1);
return projectIdentifierLength * 7 + 7 + sequenceDigits * 7; // project identifier chars + dash + sequence digits
};
/**
* Calculates the minimum width needed for issue identifiers in list layouts
* @param projectIdentifierLength - Length of the project identifier (e.g., "PROJ" = 4)
* @param maxSequenceId - Maximum sequence ID in the project (e.g., 1234)
* @returns Width in pixels needed to display the identifier
*
* @example
* // For "PROJ-1234"
* calculateIdentifierWidth(4, 1234) // Returns width for "PROJ" + "-" + "1234"
*/
export const calculateIdentifierWidth = (projectIdentifierLength: number, maxSequenceId: number): number => {
const safeMax = Number.isFinite(maxSequenceId) && maxSequenceId > 0 ? Math.floor(maxSequenceId) : 1;
const sequenceDigits = Math.max(1, Math.floor(Math.log10(safeMax)) + 1);
return projectIdentifierLength * 7 + 7 + sequenceDigits * 7; // project identifier chars + dash + sequence digits
};
🤖 Prompt for AI Agents
In apps/web/core/components/issues/issue-layouts/utils.tsx around lines 752 to
765, protect calculateIdentifierWidth from non-finite or non-positive
maxSequenceId values by clamping/coercing maxSequenceId to a finite integer >= 1
before computing digits; specifically, create a safeMax (e.g., if
Number.isFinite(maxSequenceId) && maxSequenceId > 0 use
Math.floor(maxSequenceId) else 1), then compute sequenceDigits via
Math.floor(Math.log10(safeMax)) + 1 and return the width using that value so the
function never yields NaN for inline styles.

11 changes: 11 additions & 0 deletions apps/web/core/store/project/project.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export interface IProjectStore {
joinedProjectIds: string[];
favoriteProjectIds: string[];
currentProjectDetails: TProject | undefined;
currentProjectNextSequenceId: number | undefined;
// actions
getProjectById: (projectId: string | undefined | null) => TProject | undefined;
getPartialProjectById: (projectId: string | undefined | null) => TPartialProject | undefined;
Expand Down Expand Up @@ -107,6 +108,7 @@ export class ProjectStore implements IProjectStore {
currentProjectDetails: computed,
joinedProjectIds: computed,
favoriteProjectIds: computed,
currentProjectNextSequenceId: computed,
// helper actions
processProjectAfterCreation: action,
// fetch actions
Expand Down Expand Up @@ -216,6 +218,15 @@ export class ProjectStore implements IProjectStore {
return this.projectMap?.[this.rootStore.router.projectId];
}

/**
* Returns the next sequence ID for the current project
* Used for calculating identifier width in list layouts
*/
get currentProjectNextSequenceId() {
if (!this.rootStore.router.projectId) return undefined;
return this.currentProjectDetails?.next_work_item_sequence;
}

/**
* Returns joined project IDs belong to the current workspace
*/
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/project/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export interface IProject extends IPartialProject {
is_favorite?: boolean;
members?: string[];
timezone?: string;
next_work_item_sequence?: number;
}

export type TProjectAnalyticsCountParams = {
Expand Down
Loading