[WEB-5647] chore: list layout work item identifier enhancements (#8326)

This commit is contained in:
Anmol Singh Bhatia 2025-12-12 19:21:29 +05:30 committed by GitHub
parent 1b427392c4
commit 2ac5efe2f0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 43 additions and 3 deletions

View file

@ -3,6 +3,7 @@ from rest_framework import serializers
# Module imports # Module imports
from .base import BaseSerializer, DynamicBaseSerializer from .base import BaseSerializer, DynamicBaseSerializer
from django.db.models import Max
from plane.app.serializers.workspace import WorkspaceLiteSerializer from plane.app.serializers.workspace import WorkspaceLiteSerializer
from plane.app.serializers.user import UserLiteSerializer, UserAdminLiteSerializer from plane.app.serializers.user import UserLiteSerializer, UserAdminLiteSerializer
from plane.db.models import ( from plane.db.models import (
@ -12,6 +13,7 @@ from plane.db.models import (
ProjectIdentifier, ProjectIdentifier,
DeployBoard, DeployBoard,
ProjectPublicMember, ProjectPublicMember,
IssueSequence
) )
from plane.utils.content_validator import ( from plane.utils.content_validator import (
validate_html_content, validate_html_content,
@ -105,6 +107,7 @@ class ProjectListSerializer(DynamicBaseSerializer):
members = serializers.SerializerMethodField() members = serializers.SerializerMethodField()
cover_image_url = serializers.CharField(read_only=True) cover_image_url = serializers.CharField(read_only=True)
inbox_view = serializers.BooleanField(read_only=True, source="intake_view") inbox_view = serializers.BooleanField(read_only=True, source="intake_view")
next_work_item_sequence = serializers.SerializerMethodField()
def get_members(self, obj): def get_members(self, obj):
project_members = getattr(obj, "members_list", None) project_members = getattr(obj, "members_list", None)
@ -113,6 +116,11 @@ class ProjectListSerializer(DynamicBaseSerializer):
return [member.member_id for member in project_members if member.is_active and not member.member.is_bot] return [member.member_id for member in project_members if member.is_active and not member.member.is_bot]
return [] 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: class Meta:
model = Project model = Project
fields = "__all__" fields = "__all__"

View file

@ -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"; import { IssueStats } from "@/plane-web/components/issues/issue-layouts/issue-stats";
// types // types
import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC"; import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC";
import { calculateIdentifierWidth } from "../utils";
import type { TRenderQuickActions } from "./list-view-types"; import type { TRenderQuickActions } from "./list-view-types";
interface IssueBlockProps { interface IssueBlockProps {
@ -76,7 +77,7 @@ export const IssueBlock = observer(function IssueBlock(props: IssueBlockProps) {
const projectId = routerProjectId?.toString(); const projectId = routerProjectId?.toString();
// hooks // hooks
const { sidebarCollapsed: isSidebarCollapsed } = useAppTheme(); const { sidebarCollapsed: isSidebarCollapsed } = useAppTheme();
const { getProjectIdentifierById } = useProject(); const { getProjectIdentifierById, currentProjectNextSequenceId } = useProject();
const { const {
getIsIssuePeeked, getIsIssuePeeked,
peekIssue, 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 // Calculate width for: projectIdentifier + "-" + dynamic sequence number digits
const keyMinWidth = displayProperties?.key ? (projectIdentifier?.length ?? 0) * 7 : 0; // 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({ const workItemLink = generateWorkItemLink({
workspaceSlug, workspaceSlug,

View file

@ -748,3 +748,18 @@ export const isFiltersApplied = (filters: IIssueFilterOptions): boolean =>
if (Array.isArray(value)) return value.length > 0; if (Array.isArray(value)) return value.length > 0;
return value !== undefined && value !== null && value !== ""; 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
};

View file

@ -30,6 +30,7 @@ export interface IProjectStore {
joinedProjectIds: string[]; joinedProjectIds: string[];
favoriteProjectIds: string[]; favoriteProjectIds: string[];
currentProjectDetails: TProject | undefined; currentProjectDetails: TProject | undefined;
currentProjectNextSequenceId: number | undefined;
// actions // actions
getProjectById: (projectId: string | undefined | null) => TProject | undefined; getProjectById: (projectId: string | undefined | null) => TProject | undefined;
getPartialProjectById: (projectId: string | undefined | null) => TPartialProject | undefined; getPartialProjectById: (projectId: string | undefined | null) => TPartialProject | undefined;
@ -107,6 +108,7 @@ export class ProjectStore implements IProjectStore {
currentProjectDetails: computed, currentProjectDetails: computed,
joinedProjectIds: computed, joinedProjectIds: computed,
favoriteProjectIds: computed, favoriteProjectIds: computed,
currentProjectNextSequenceId: computed,
// helper actions // helper actions
processProjectAfterCreation: action, processProjectAfterCreation: action,
// fetch actions // fetch actions
@ -216,6 +218,15 @@ export class ProjectStore implements IProjectStore {
return this.projectMap?.[this.rootStore.router.projectId]; 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 * Returns joined project IDs belong to the current workspace
*/ */

View file

@ -51,6 +51,7 @@ export interface IProject extends IPartialProject {
is_favorite?: boolean; is_favorite?: boolean;
members?: string[]; members?: string[];
timezone?: string; timezone?: string;
next_work_item_sequence?: number;
} }
export type TProjectAnalyticsCountParams = { export type TProjectAnalyticsCountParams = {