diff --git a/HDESIGN-CODE.md b/HDESIGN-CODE.md index f512472..ffe5677 100644 --- a/HDESIGN-CODE.md +++ b/HDESIGN-CODE.md @@ -242,6 +242,7 @@ ## Drag and drop - Drag overlay использует акцентный контур. +- Во внутреннем kanban после успешного ручного переноса карточка остается в активной заливке `active_card_rgb` до выбора другой карточки/следующего переноса; сам drag-жест не открывает detail pane. - Delete dropzone: - без красного технического свечения и без red-tinted text/fill - текст локализован diff --git a/plane-src/apps/web/core/components/issues/issue-layouts/kanban/base-kanban-root.tsx b/plane-src/apps/web/core/components/issues/issue-layouts/kanban/base-kanban-root.tsx index ee022c0..ac3c26e 100644 --- a/plane-src/apps/web/core/components/issues/issue-layouts/kanban/base-kanban-root.tsx +++ b/plane-src/apps/web/core/components/issues/issue-layouts/kanban/base-kanban-root.tsx @@ -30,6 +30,7 @@ import { DeleteIssueModal } from "../../delete-issue-modal"; import { IssueLayoutHOC } from "../issue-layout-HOC"; import type { IQuickActionProps, TRenderQuickActions } from "../list/list-view-types"; //components +import type { GroupDropLocation } from "../utils"; import { getSourceFromDropPayload } from "../utils"; import { KanBan } from "./default"; import { KanBanSwimLanes } from "./swimlanes"; @@ -127,6 +128,7 @@ export const BaseKanBanRoot = observer(function BaseKanBanRoot(props: IBaseKanBa // states const [draggedIssueId, setDraggedIssueId] = useState(undefined); + const [selectedIssueId, setSelectedIssueId] = useState(undefined); const [deleteIssueModal, setDeleteIssueModal] = useState(false); const isEditingAllowed = allowPermissions( @@ -136,6 +138,18 @@ export const BaseKanBanRoot = observer(function BaseKanBanRoot(props: IBaseKanBa const handleOnDrop = useGroupIssuesDragNDrop(storeType, orderBy, group_by, sub_group_by); + const handleIssueDrop = useCallback( + async (source: GroupDropLocation, destination: GroupDropLocation) => { + if (cardVariant === "internal-contour") setSelectedIssueId(source.id); + await handleOnDrop(source, destination); + }, + [cardVariant, handleOnDrop] + ); + + const handleClearSelectedIssue = useCallback(() => { + setSelectedIssueId(undefined); + }, []); + const canEditProperties = useCallback( (projectId: string | undefined) => { const isEditingAllowedBasedOnProject = @@ -305,7 +319,9 @@ export const BaseKanBanRoot = observer(function BaseKanBanRoot(props: IBaseKanBa addIssuesToView={addIssuesToView} cardVariant={cardVariant} scrollableContainerRef={scrollableContainerRef} - handleOnDrop={handleOnDrop} + handleOnDrop={handleIssueDrop} + selectedIssueId={selectedIssueId} + onClearSelectedIssue={handleClearSelectedIssue} loadMoreIssues={fetchMoreIssues} isEpic={isEpic} /> diff --git a/plane-src/apps/web/core/components/issues/issue-layouts/kanban/block.tsx b/plane-src/apps/web/core/components/issues/issue-layouts/kanban/block.tsx index d069f59..f4a8c9f 100644 --- a/plane-src/apps/web/core/components/issues/issue-layouts/kanban/block.tsx +++ b/plane-src/apps/web/core/components/issues/issue-layouts/kanban/block.tsx @@ -62,6 +62,8 @@ interface IssueBlockProps { shouldRenderByDefault?: boolean; isEpic?: boolean; cardVariant?: TKanbanCardVariant; + selectedIssueId?: string; + onClearSelectedIssue?: () => void; } interface IssueDetailsBlockProps { @@ -198,6 +200,8 @@ export const KanbanIssueBlock = observer(function KanbanIssueBlock(props: IssueB shouldRenderByDefault, isEpic = false, cardVariant = "default", + selectedIssueId, + onClearSelectedIssue, } = props; const cardRef = useRef(null); @@ -225,6 +229,14 @@ export const KanbanIssueBlock = observer(function KanbanIssueBlock(props: IssueB const isDragAllowed = canDragIssuesInCurrentGrouping && !issue?.tempId && canEditIssueProperties; const projectIdentifier = getProjectIdentifierById(issue?.project_id); const isPeeked = issue ? getIsIssuePeeked(issue.id) : false; + const isDropSelected = cardVariant === "internal-contour" && selectedIssueId === issue?.id; + + const handleIssueBlockClick = () => { + if (!issue) return; + + if (selectedIssueId && selectedIssueId !== issue.id) onClearSelectedIssue?.(); + handleIssuePeekOverview(issue); + }; const workItemLink = generateWorkItemLink({ workspaceSlug, @@ -318,7 +330,7 @@ export const KanbanIssueBlock = observer(function KanbanIssueBlock(props: IssueB { "z-[100]": isCurrentBlockDragging && cardVariant === "internal-contour" } )} data-card-variant={cardVariant} - onClick={() => handleIssuePeekOverview(issue)} + onClick={handleIssueBlockClick} disabled={!!issue?.tempId} > diff --git a/plane-src/apps/web/core/components/issues/issue-layouts/kanban/blocks-list.tsx b/plane-src/apps/web/core/components/issues/issue-layouts/kanban/blocks-list.tsx index 616cf9e..320e426 100644 --- a/plane-src/apps/web/core/components/issues/issue-layouts/kanban/blocks-list.tsx +++ b/plane-src/apps/web/core/components/issues/issue-layouts/kanban/blocks-list.tsx @@ -23,6 +23,8 @@ interface IssueBlocksListProps { quickActions: TRenderQuickActions; canEditProperties: (projectId: string | undefined) => boolean; cardVariant?: TKanbanCardVariant; + selectedIssueId?: string; + onClearSelectedIssue?: () => void; canDropOverIssue: boolean; canDragIssuesInCurrentGrouping: boolean; scrollableContainerRef?: MutableRefObject; @@ -42,6 +44,8 @@ export const KanbanIssueBlocksList = observer(function KanbanIssueBlocksList(pro quickActions, canEditProperties, cardVariant = "default", + selectedIssueId, + onClearSelectedIssue, scrollableContainerRef, isEpic = false, } = props; @@ -70,6 +74,8 @@ export const KanbanIssueBlocksList = observer(function KanbanIssueBlocksList(pro quickActions={quickActions} draggableId={draggableId} cardVariant={cardVariant} + selectedIssueId={selectedIssueId} + onClearSelectedIssue={onClearSelectedIssue} canDropOverIssue={canDropOverIssue} canDragIssuesInCurrentGrouping={canDragIssuesInCurrentGrouping} canEditProperties={canEditProperties} diff --git a/plane-src/apps/web/core/components/issues/issue-layouts/kanban/default.tsx b/plane-src/apps/web/core/components/issues/issue-layouts/kanban/default.tsx index 0439fce..1bb6e14 100644 --- a/plane-src/apps/web/core/components/issues/issue-layouts/kanban/default.tsx +++ b/plane-src/apps/web/core/components/issues/issue-layouts/kanban/default.tsx @@ -64,6 +64,8 @@ export interface IKanBan { addIssuesToView?: (issueIds: string[]) => Promise; canEditProperties: (projectId: string | undefined) => boolean; cardVariant?: TKanbanCardVariant; + selectedIssueId?: string; + onClearSelectedIssue?: () => void; scrollableContainerRef?: MutableRefObject; handleOnDrop: (source: GroupDropLocation, destination: GroupDropLocation) => Promise; showEmptyGroup?: boolean; @@ -91,6 +93,8 @@ export const KanBan = observer(function KanBan(props: IKanBan) { addIssuesToView, canEditProperties, cardVariant = "default", + selectedIssueId, + onClearSelectedIssue, scrollableContainerRef, handleOnDrop, showEmptyGroup = true, @@ -228,6 +232,8 @@ export const KanBan = observer(function KanBan(props: IKanBan) { disableIssueCreation={disableIssueCreation} canEditProperties={canEditProperties} cardVariant={cardVariant} + selectedIssueId={selectedIssueId} + onClearSelectedIssue={onClearSelectedIssue} scrollableContainerRef={scrollableContainerRef} loadMoreIssues={loadMoreIssues} handleOnDrop={handleOnDrop} diff --git a/plane-src/apps/web/core/components/issues/issue-layouts/kanban/kanban-group.tsx b/plane-src/apps/web/core/components/issues/issue-layouts/kanban/kanban-group.tsx index ca60420..341d21d 100644 --- a/plane-src/apps/web/core/components/issues/issue-layouts/kanban/kanban-group.tsx +++ b/plane-src/apps/web/core/components/issues/issue-layouts/kanban/kanban-group.tsx @@ -67,6 +67,8 @@ interface IKanbanGroup { disableIssueCreation?: boolean; canEditProperties: (projectId: string | undefined) => boolean; cardVariant?: TKanbanCardVariant; + selectedIssueId?: string; + onClearSelectedIssue?: () => void; groupByVisibilityToggle?: boolean; scrollableContainerRef?: MutableRefObject; handleOnDrop: (source: GroupDropLocation, destination: GroupDropLocation) => Promise; @@ -90,6 +92,8 @@ export const KanbanGroup = observer(function KanbanGroup(props: IKanbanGroup) { quickActions, canEditProperties, cardVariant = "default", + selectedIssueId, + onClearSelectedIssue, loadMoreIssues, enableQuickIssueCreate, disableIssueCreation, @@ -314,6 +318,8 @@ export const KanbanGroup = observer(function KanbanGroup(props: IKanbanGroup) { quickActions={quickActions} canEditProperties={canEditProperties} cardVariant={cardVariant} + selectedIssueId={selectedIssueId} + onClearSelectedIssue={onClearSelectedIssue} scrollableContainerRef={scrollableContainerRef} canDropOverIssue={!canOverlayBeVisible} canDragIssuesInCurrentGrouping={canDragIssuesInCurrentGrouping} diff --git a/plane-src/apps/web/core/components/issues/issue-layouts/kanban/swimlanes.tsx b/plane-src/apps/web/core/components/issues/issue-layouts/kanban/swimlanes.tsx index fadbea5..2c12c0a 100644 --- a/plane-src/apps/web/core/components/issues/issue-layouts/kanban/swimlanes.tsx +++ b/plane-src/apps/web/core/components/issues/issue-layouts/kanban/swimlanes.tsx @@ -128,6 +128,8 @@ interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader { quickActions: TRenderQuickActions; quickAddCallback?: (projectId: string | null | undefined, data: TIssue) => Promise; scrollableContainerRef?: MutableRefObject; + selectedIssueId?: string; + onClearSelectedIssue?: () => void; showEmptyGroup: boolean; updateIssue: ((projectId: string | null, issueId: string, data: Partial) => Promise) | undefined; } @@ -154,6 +156,8 @@ const SubGroupSwimlane = observer(function SubGroupSwimlane(props: ISubGroupSwim quickActions, quickAddCallback, scrollableContainerRef, + selectedIssueId, + onClearSelectedIssue, showEmptyGroup, sub_group_by, updateIssue, @@ -223,6 +227,8 @@ const SubGroupSwimlane = observer(function SubGroupSwimlane(props: ISubGroupSwim addIssuesToView={addIssuesToView} quickAddCallback={quickAddCallback} scrollableContainerRef={scrollableContainerRef} + selectedIssueId={selectedIssueId} + onClearSelectedIssue={onClearSelectedIssue} loadMoreIssues={loadMoreIssues} handleOnDrop={handleOnDrop} orderBy={orderBy} @@ -263,6 +269,8 @@ export interface IKanBanSwimLanes { quickActions: TRenderQuickActions; quickAddCallback?: (projectId: string | null | undefined, data: TIssue) => Promise; scrollableContainerRef?: MutableRefObject; + selectedIssueId?: string; + onClearSelectedIssue?: () => void; showEmptyGroup: boolean; sub_group_by: TIssueGroupByOptions | undefined; updateIssue: ((projectId: string | null, issueId: string, data: Partial) => Promise) | undefined; @@ -291,6 +299,8 @@ export const KanBanSwimLanes = observer(function KanBanSwimLanes(props: IKanBanS addIssuesToView, quickAddCallback, scrollableContainerRef, + selectedIssueId, + onClearSelectedIssue, isEpic = false, } = props; // store hooks @@ -350,6 +360,8 @@ export const KanBanSwimLanes = observer(function KanBanSwimLanes(props: IKanBanS cardVariant={cardVariant} quickAddCallback={quickAddCallback} scrollableContainerRef={scrollableContainerRef} + selectedIssueId={selectedIssueId} + onClearSelectedIssue={onClearSelectedIssue} isEpic={isEpic} /> )} diff --git a/plane-src/apps/web/core/components/issues/issue-layouts/utils.tsx b/plane-src/apps/web/core/components/issues/issue-layouts/utils.tsx index 718322b..0c5db7e 100644 --- a/plane-src/apps/web/core/components/issues/issue-layouts/utils.tsx +++ b/plane-src/apps/web/core/components/issues/issue-layouts/utils.tsx @@ -355,12 +355,15 @@ export const highlightIssueOnDrop = ( ) => { setTimeout(async () => { const sourceElementId = elementId ?? ""; - const sourceElement = document.getElementById(sourceElementId); + let sourceElement = document.getElementById(sourceElementId); + + for (let attempt = 0; !sourceElement && attempt < 8; attempt++) { + await new Promise((resolve) => setTimeout(resolve, 100)); + sourceElement = document.getElementById(sourceElementId); + } if (sourceElement?.dataset.cardVariant === "internal-contour") { - sourceElement.classList.remove(HIGHLIGHT_CLASS, HIGHLIGHT_WITH_LINE); - sourceElement.classList.add(NODEDC_DROP_FILL_HIGHLIGHT_CLASS); - window.setTimeout(() => sourceElement.classList.remove(NODEDC_DROP_FILL_HIGHLIGHT_CLASS), 1600); + sourceElement.classList.remove(HIGHLIGHT_CLASS, HIGHLIGHT_WITH_LINE, NODEDC_DROP_FILL_HIGHLIGHT_CLASS); if (shouldScrollIntoView) await scrollIntoView(sourceElement, { behavior: "smooth", block: "center", duration: 1500 });