diff --git a/apps/api/plane/app/serializers/project.py b/apps/api/plane/app/serializers/project.py index 01569cbc96a..d7c458d40d1 100644 --- a/apps/api/plane/app/serializers/project.py +++ b/apps/api/plane/app/serializers/project.py @@ -3,6 +3,7 @@ # Module imports from .base import BaseSerializer, DynamicBaseSerializer +from django.db.models import Max from plane.app.serializers.workspace import WorkspaceLiteSerializer from plane.app.serializers.user import UserLiteSerializer, UserAdminLiteSerializer from plane.db.models import ( @@ -12,6 +13,7 @@ ProjectIdentifier, DeployBoard, ProjectPublicMember, + IssueSequence ) from plane.utils.content_validator import ( validate_html_content, @@ -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) @@ -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 + class Meta: model = Project fields = "__all__" diff --git a/apps/web/core/components/issues/issue-layouts/list/block.tsx b/apps/web/core/components/issues/issue-layouts/list/block.tsx index 1b7cae0fd7e..b8352c51442 100644 --- a/apps/web/core/components/issues/issue-layouts/list/block.tsx +++ b/apps/web/core/components/issues/issue-layouts/list/block.tsx @@ -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 { @@ -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, @@ -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, diff --git a/apps/web/core/components/issues/issue-layouts/utils.tsx b/apps/web/core/components/issues/issue-layouts/utils.tsx index 524af27da27..564767f3b92 100644 --- a/apps/web/core/components/issues/issue-layouts/utils.tsx +++ b/apps/web/core/components/issues/issue-layouts/utils.tsx @@ -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); + return projectIdentifierLength * 7 + 7 + sequenceDigits * 7; // project identifier chars + dash + sequence digits +}; diff --git a/apps/web/core/store/project/project.store.ts b/apps/web/core/store/project/project.store.ts index 3dce91b05a8..422fac379fa 100644 --- a/apps/web/core/store/project/project.store.ts +++ b/apps/web/core/store/project/project.store.ts @@ -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; @@ -107,6 +108,7 @@ export class ProjectStore implements IProjectStore { currentProjectDetails: computed, joinedProjectIds: computed, favoriteProjectIds: computed, + currentProjectNextSequenceId: computed, // helper actions processProjectAfterCreation: action, // fetch actions @@ -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 */ diff --git a/packages/types/src/project/projects.ts b/packages/types/src/project/projects.ts index aa4d1bcba0d..9da12b57033 100644 --- a/packages/types/src/project/projects.ts +++ b/packages/types/src/project/projects.ts @@ -51,6 +51,7 @@ export interface IProject extends IPartialProject { is_favorite?: boolean; members?: string[]; timezone?: string; + next_work_item_sequence?: number; } export type TProjectAnalyticsCountParams = {