Skip to content
Open
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: 4 additions & 4 deletions apps/web/core/components/core/list/list-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,20 +74,20 @@ export function ListItem(props: IListItemProps) {
className
)}
>
<div className={cn("relative flex w-full items-center justify-between gap-3 truncate ", itemClassName)}>
<div className={cn("relative flex w-full items-center justify-between gap-3 ", itemClassName)}>
<ControlLink
id={id}
className="relative flex w-full items-center gap-3 overflow-hidden"
className="relative flex w-full items-center gap-3"
href={itemLink}
target="_self"
onClick={handleControlLinkClick}
disabled={disableLink}
data-prevent-progress={preventDefaultProgress}
>
<div className={cn("flex items-center gap-4 truncate", leftElementClassName)}>
<div className={cn("flex items-center gap-4", leftElementClassName)}>
{prependTitleElement && <span className="flex items-center flex-shrink-0">{prependTitleElement}</span>}
<Tooltip tooltipContent={title} position="top" isMobile={isMobile}>
<span className="truncate text-13">{title}</span>
<span className="text-13 select-text">{title}</span>
</Tooltip>
</div>
{appendTitleElement && (
Expand Down
36 changes: 36 additions & 0 deletions apps/web/core/components/issues/issue-layouts/list/block.logic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* Determines wheter a pointer/drag event should be suppressed.
* If user has any text selected. we avoid triggering drag behavior
*/
export function shouldSuppressEvent(selectionText: string | null): boolean {
return !!selectionText && selectionText.length > 0;
}
/**
* If the user has selected text, dragging is always disabled
*/
export function canDragBasedOnSelection(selectionText: string | null, isAllowed: boolean) {
if (selectionText) return false;
return isAllowed;
}

/**
* Computes the next expand/collapse state for a list item
*/
export function nextExpandState(nestingLevel: number, isExpanded: boolean) {
return nestingLevel >= 3 ? isExpanded : !isExpanded;
}

/**
* If dragging is allowed returns null
* Otherwise the message depends on edit permissions
*/
export function getDragDisallowedToast(canDrag: boolean, canEdit: boolean) {
if (canDrag) return null;

return {
title: "Cannot move work item",
message: canEdit
? "Drag and drop is disabled for the current grouping"
: "You are not allowed to move this work item",
};
}
214 changes: 129 additions & 85 deletions apps/web/core/components/issues/issue-layouts/list/block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export const IssueBlock = observer(function IssueBlock(props: IssueBlockProps) {
displayProperties,
canEditProperties,
nestingLevel,
spacingLeft = 14,
//spacingLeft = 14,
isExpanded,
setExpanded,
selectionHelpers,
Expand Down Expand Up @@ -121,7 +121,10 @@ export const IssueBlock = observer(function IssueBlock(props: IssueBlockProps) {
return combine(
draggable({
element,
canDrag: () => isDraggingAllowed,
canDrag: () => {
if (window.getSelection()?.toString()) return false;
return isDraggingAllowed;
},
Comment on lines 121 to +127
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

This makes the drag hotspot effectively row-wide again.

draggable() is now attached to an invisible absolute element that spans the main content area, so dragging can start from almost any blank space in the row instead of a dedicated handle. That reintroduces the same click/selection conflicts this change is trying to eliminate.

Also applies to: 223-227

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/core/components/issues/issue-layouts/list/block.tsx` around lines
121 - 127, The draggable() is being applied to an invisible absolute element
(the variable element) so the drag hotspot spans the whole row; instead, limit
dragging to the visible handle by attaching draggable() to the actual handle
node or by guarding canDrag to only allow drag when the event target is the
handle (e.g., check event.target or a handleRef/class) and keep checking
window.getSelection() and isDraggingAllowed; apply the same fix where
draggable(...) is used later (the other combine call around the second element
usage) so only the handle can initiate drags.

getInitialData: () => ({ id: issueId, type: "ISSUE", groupId }),
onDragStart: () => {
setIsCurrentBlockDragging(true);
Expand All @@ -138,10 +141,10 @@ export const IssueBlock = observer(function IssueBlock(props: IssueBlockProps) {
const projectIdentifier = getProjectIdentifierById(issue.project_id);
const isIssueSelected = selectionHelpers.getIsEntitySelected(issue.id);
const isIssueActive = selectionHelpers.getIsEntityActive(issue.id);
const isSubIssue = nestingLevel !== 0;
//const isSubIssue = nestingLevel !== 0;
const canSelectIssues = canEditIssueProperties && !selectionHelpers.isSelectionDisabled;

const marginLeft = `${spacingLeft}px`;
//const marginLeft = `${spacingLeft}px`;

const handleToggleExpand = (e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
Expand All @@ -151,7 +154,7 @@ export const IssueBlock = observer(function IssueBlock(props: IssueBlockProps) {
} else {
setExpanded((prevState) => {
if (!prevState && workspaceSlug && issue && issue.project_id)
subIssuesStore.fetchSubIssues(workspaceSlug.toString(), issue.project_id, issue.id);
void subIssuesStore.fetchSubIssues(workspaceSlug.toString(), issue.project_id, issue.id);
return !prevState;
});
}
Expand All @@ -177,12 +180,20 @@ export const IssueBlock = observer(function IssueBlock(props: IssueBlockProps) {
<ControlLink
id={`issue-${issue.id}`}
href={workItemLink}
onClick={() => handleIssuePeekOverview(issue)}
onClick={(e) => {
const selection = window.getSelection();
if (selection && selection.toString().length > 0) {
e.preventDefault();
e.stopPropagation();
return;
}

handleIssuePeekOverview(issue);
}}
className="w-full cursor-pointer"
disabled={!!issue?.tempId || issue?.is_draft}
>
<Row
ref={issueRef}
className={cn(
"group/list-block min-h-11 relative flex flex-col gap-3 bg-layer-transparent hover:bg-layer-transparent-hover py-3 text-13 transition-colors",
{
Expand All @@ -207,92 +218,120 @@ export const IssueBlock = observer(function IssueBlock(props: IssueBlockProps) {
}
}}
>
<div className="flex gap-2 w-full truncate">
<div className="flex flex-grow items-center gap-0.5 truncate">
<div className="flex items-center gap-1" style={isSubIssue ? { marginLeft } : {}}>
{/* select checkbox */}
{projectId && canSelectIssues && !isEpic && (
<Tooltip
tooltipContent={
<>
Only work items within the current
<br />
project can be selected.
</>
<div className="flex gap-2 w-full">
<div className=" relative flex items-center gap-2" style={{ flexBasis: "95%" }}>
<div
ref={issueRef}
className="absolute left-0 top-0 h-full"
style={{ width: "100%", pointerEvents: "auto" }}
/>
<div className="flex items-center gap-1 relative z-10 pointer-events-none">
{/* eslint-disable jsx-a11y/no-static-element-interactions */}
{/* eslint-disable jsx-a11y/click-events-have-key-events */}
<div
className="flex items-center gap-1 pointer-events-auto"
onClick={(e) => {
if (window.getSelection()?.toString()) {
e.preventDefault();
e.stopPropagation();
}
disabled={issue.project_id === projectId}
>
<div className="flex-shrink-0 grid place-items-center w-3.5 absolute left-1">
<MultipleSelectEntityAction
className={cn(
"opacity-0 pointer-events-none group-hover/list-block:opacity-100 group-hover/list-block:pointer-events-auto transition-opacity",
{
"opacity-100 pointer-events-auto": isIssueSelected,
}
)}
groupId={groupId}
id={issue.id}
selectionHelpers={selectionHelpers}
disabled={issue.project_id !== projectId}
/>
}}
>
{/* select checkbox */}
{projectId && canSelectIssues && !isEpic && (
<Tooltip
tooltipContent={
<>
Only work items within the current
<br />
project can be selected.
</>
}
disabled={issue.project_id === projectId}
>
<div className="flex-shrink-0 grid place-items-center w-3.5 absolute left-1 pointer-events-auto">
<MultipleSelectEntityAction
className={cn(
"opacity-0 pointer-events-none group-hover/list-block:opacity-100 group-hover/list-block:pointer-events-auto transition-opacity",
{
"opacity-100 pointer-events-auto": isIssueSelected,
}
)}
groupId={groupId}
id={issue.id}
selectionHelpers={selectionHelpers}
disabled={issue.project_id !== projectId}
/>
</div>
</Tooltip>
)}
{displayProperties && (displayProperties.key || displayProperties.issue_type) && (
<div className="select-text flex-shrink-0" style={{ minWidth: `${keyMinWidth}px` }}>
{issue.project_id && (
<IssueIdentifier
issueId={issueId}
projectId={issue.project_id}
size="xs"
variant="tertiary"
displayProperties={displayProperties}
/>
)}
</div>
</Tooltip>
)}
{displayProperties && (displayProperties.key || displayProperties.issue_type) && (
<div className="flex-shrink-0" style={{ minWidth: `${keyMinWidth}px` }}>
{issue.project_id && (
<IssueIdentifier
issueId={issueId}
projectId={issue.project_id}
size="xs"
variant="tertiary"
displayProperties={displayProperties}
/>
)}

{/* sub-issues chevron */}
<div className="size-4 grid place-items-center flex-shrink-0 pointer-events-auto">
{subIssuesCount > 0 && !isEpic && (
<button
type="button"
className="size-4 grid place-items-center rounded-xs text-placeholder hover:text-tertiary"
onClick={(e) => {
if (window.getSelection()?.toString()) {
e.preventDefault();
e.stopPropagation();
return;
}
handleToggleExpand(e);
}}
>
<ChevronRightIcon
className={cn("size-4", {
"rotate-90": isExpanded,
})}
strokeWidth={2.5}
/>
</button>
)}
</div>
)}

{/* sub-issues chevron */}
<div className="size-4 grid place-items-center flex-shrink-0">
{subIssuesCount > 0 && !isEpic && (
<button
type="button"
className="size-4 grid place-items-center rounded-xs text-placeholder hover:text-tertiary"
onClick={handleToggleExpand}
{issue?.tempId !== undefined && (
<div className="absolute left-0 top-0 z-[99999] h-full w-full animate-pulse bg-surface-1/20" />
)}
<Tooltip
tooltipContent={issue.name}
isMobile={isMobile}
position="top-start"
disabled={isCurrentBlockDragging}
renderByDefault={false}
>
<p
className="select-text cursor-text text-body-xs-medium text-primary pointer-events-auto"
style={{ userSelect: "text" }}
>
{issue.name}
</p>
</Tooltip>
{isEpic && displayProperties && (
<WithDisplayPropertiesHOC
displayProperties={displayProperties}
displayPropertyKey="sub_issue_count"
shouldRenderProperty={(properties) => !!properties.sub_issue_count}
>
Comment on lines +324 to 329
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Keep epic stats hidden when the sub-issue count is zero.

This guard now only checks the display flag, so IssueStats renders for every epic even when subIssuesCount is 0. The Kanban block keeps the old behavior by also gating on !!subIssuesCount.

Suggested fix
                 {isEpic && displayProperties && (
                   <WithDisplayPropertiesHOC
                     displayProperties={displayProperties}
                     displayPropertyKey="sub_issue_count"
-                    shouldRenderProperty={(properties) => !!properties.sub_issue_count}
+                    shouldRenderProperty={(properties) =>
+                      !!properties.sub_issue_count && !!subIssuesCount
+                    }
                   >
📝 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
{isEpic && displayProperties && (
<WithDisplayPropertiesHOC
displayProperties={displayProperties}
displayPropertyKey="sub_issue_count"
shouldRenderProperty={(properties) => !!properties.sub_issue_count}
>
{isEpic && displayProperties && (
<WithDisplayPropertiesHOC
displayProperties={displayProperties}
displayPropertyKey="sub_issue_count"
shouldRenderProperty={(properties) =>
!!properties.sub_issue_count && !!subIssuesCount
}
>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/core/components/issues/issue-layouts/list/block.tsx` around lines
324 - 329, The epic stats are shown when displayProperties is true even if the
actual count is zero; update the WithDisplayPropertiesHOC guard so it also
checks the runtime sub-issue count. Change the shouldRenderProperty callback
used with displayPropertyKey="sub_issue_count" to return a boolean that combines
the display property and the local sub-issue count (e.g., shouldRenderProperty =
(properties) => !!properties.sub_issue_count && !!subIssuesCount) so IssueStats
stays hidden when subIssuesCount is 0.

<ChevronRightIcon
className={cn("size-4", {
"rotate-90": isExpanded,
})}
strokeWidth={2.5}
/>
</button>
<IssueStats issueId={issue.id} className="ml-2 text-body-xs-medium text-tertiary" />
</WithDisplayPropertiesHOC>
)}
</div>

{issue?.tempId !== undefined && (
<div className="absolute left-0 top-0 z-[99999] h-full w-full animate-pulse bg-surface-1/20" />
)}
</div>

<Tooltip
tooltipContent={issue.name}
isMobile={isMobile}
position="top-start"
disabled={isCurrentBlockDragging}
renderByDefault={false}
>
<p className="truncate cursor-pointer text-body-xs-medium text-primary">{issue.name}</p>
</Tooltip>
{isEpic && displayProperties && (
<WithDisplayPropertiesHOC
displayProperties={displayProperties}
displayPropertyKey="sub_issue_count"
shouldRenderProperty={(properties) => !!properties.sub_issue_count}
>
<IssueStats issueId={issue.id} className="ml-2 text-body-xs-medium text-tertiary" />
</WithDisplayPropertiesHOC>
)}
</div>
{!issue?.tempId && (
<div
Expand All @@ -308,7 +347,7 @@ export const IssueBlock = observer(function IssueBlock(props: IssueBlockProps) {
</div>
)}
</div>
<div className="flex flex-shrink-0 items-center gap-2">
<div className="flex flex-shrink-0 items-center gap-2" onMouseDown={(e) => e.stopPropagation()}>
{!issue?.tempId ? (
<>
<IssueProperties
Expand All @@ -326,6 +365,11 @@ export const IssueBlock = observer(function IssueBlock(props: IssueBlockProps) {
"lg:flex": !isSidebarCollapsed,
})}
onClick={(e) => {
if (window.getSelection()?.toString()) {
e.preventDefault();
e.stopPropagation();
return;
}
e.preventDefault();
e.stopPropagation();
}}
Expand Down
Loading