[WEB-5647] chore: list layout work item identifier enhancements (#8326)
This commit is contained in:
parent
1b427392c4
commit
2ac5efe2f0
5 changed files with 43 additions and 3 deletions
|
|
@ -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__"
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue