UI - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: устойчивое выделение карточки после drag-drop
This commit is contained in:
parent
eac010d3d4
commit
f060d4dedd
|
|
@ -242,6 +242,7 @@
|
|||
|
||||
## Drag and drop
|
||||
- Drag overlay использует акцентный контур.
|
||||
- Во внутреннем kanban после успешного ручного переноса карточка остается в активной заливке `active_card_rgb` до выбора другой карточки/следующего переноса; сам drag-жест не открывает detail pane.
|
||||
- Delete dropzone:
|
||||
- без красного технического свечения и без red-tinted text/fill
|
||||
- текст локализован
|
||||
|
|
|
|||
|
|
@ -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<string | undefined>(undefined);
|
||||
const [selectedIssueId, setSelectedIssueId] = useState<string | undefined>(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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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<HTMLAnchorElement | null>(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}
|
||||
>
|
||||
<RenderIfVisible
|
||||
|
|
@ -337,7 +349,7 @@ export const KanbanIssueBlock = observer(function KanbanIssueBlock(props: IssueB
|
|||
quickActions={quickActions}
|
||||
isReadOnly={!canEditIssueProperties}
|
||||
isEpic={isEpic}
|
||||
isActive={isPeeked || (cardVariant === "internal-contour" && isCurrentBlockDragging)}
|
||||
isActive={isPeeked || isDropSelected || (cardVariant === "internal-contour" && isCurrentBlockDragging)}
|
||||
cardVariant={cardVariant}
|
||||
/>
|
||||
</RenderIfVisible>
|
||||
|
|
|
|||
|
|
@ -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<HTMLDivElement | null>;
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -64,6 +64,8 @@ export interface IKanBan {
|
|||
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
|
||||
canEditProperties: (projectId: string | undefined) => boolean;
|
||||
cardVariant?: TKanbanCardVariant;
|
||||
selectedIssueId?: string;
|
||||
onClearSelectedIssue?: () => void;
|
||||
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
||||
handleOnDrop: (source: GroupDropLocation, destination: GroupDropLocation) => Promise<void>;
|
||||
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}
|
||||
|
|
|
|||
|
|
@ -67,6 +67,8 @@ interface IKanbanGroup {
|
|||
disableIssueCreation?: boolean;
|
||||
canEditProperties: (projectId: string | undefined) => boolean;
|
||||
cardVariant?: TKanbanCardVariant;
|
||||
selectedIssueId?: string;
|
||||
onClearSelectedIssue?: () => void;
|
||||
groupByVisibilityToggle?: boolean;
|
||||
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
||||
handleOnDrop: (source: GroupDropLocation, destination: GroupDropLocation) => Promise<void>;
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -128,6 +128,8 @@ interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader {
|
|||
quickActions: TRenderQuickActions;
|
||||
quickAddCallback?: (projectId: string | null | undefined, data: TIssue) => Promise<TIssue | undefined>;
|
||||
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
||||
selectedIssueId?: string;
|
||||
onClearSelectedIssue?: () => void;
|
||||
showEmptyGroup: boolean;
|
||||
updateIssue: ((projectId: string | null, issueId: string, data: Partial<TIssue>) => Promise<void>) | 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<TIssue | undefined>;
|
||||
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
||||
selectedIssueId?: string;
|
||||
onClearSelectedIssue?: () => void;
|
||||
showEmptyGroup: boolean;
|
||||
sub_group_by: TIssueGroupByOptions | undefined;
|
||||
updateIssue: ((projectId: string | null, issueId: string, data: Partial<TIssue>) => Promise<void>) | 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}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
Loading…
Reference in New Issue