UI - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: устойчивое выделение карточки после drag-drop
This commit is contained in:
parent
eac010d3d4
commit
f060d4dedd
|
|
@ -242,6 +242,7 @@
|
||||||
|
|
||||||
## Drag and drop
|
## Drag and drop
|
||||||
- Drag overlay использует акцентный контур.
|
- Drag overlay использует акцентный контур.
|
||||||
|
- Во внутреннем kanban после успешного ручного переноса карточка остается в активной заливке `active_card_rgb` до выбора другой карточки/следующего переноса; сам drag-жест не открывает detail pane.
|
||||||
- Delete dropzone:
|
- Delete dropzone:
|
||||||
- без красного технического свечения и без red-tinted text/fill
|
- без красного технического свечения и без red-tinted text/fill
|
||||||
- текст локализован
|
- текст локализован
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ import { DeleteIssueModal } from "../../delete-issue-modal";
|
||||||
import { IssueLayoutHOC } from "../issue-layout-HOC";
|
import { IssueLayoutHOC } from "../issue-layout-HOC";
|
||||||
import type { IQuickActionProps, TRenderQuickActions } from "../list/list-view-types";
|
import type { IQuickActionProps, TRenderQuickActions } from "../list/list-view-types";
|
||||||
//components
|
//components
|
||||||
|
import type { GroupDropLocation } from "../utils";
|
||||||
import { getSourceFromDropPayload } from "../utils";
|
import { getSourceFromDropPayload } from "../utils";
|
||||||
import { KanBan } from "./default";
|
import { KanBan } from "./default";
|
||||||
import { KanBanSwimLanes } from "./swimlanes";
|
import { KanBanSwimLanes } from "./swimlanes";
|
||||||
|
|
@ -127,6 +128,7 @@ export const BaseKanBanRoot = observer(function BaseKanBanRoot(props: IBaseKanBa
|
||||||
|
|
||||||
// states
|
// states
|
||||||
const [draggedIssueId, setDraggedIssueId] = useState<string | undefined>(undefined);
|
const [draggedIssueId, setDraggedIssueId] = useState<string | undefined>(undefined);
|
||||||
|
const [selectedIssueId, setSelectedIssueId] = useState<string | undefined>(undefined);
|
||||||
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
|
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
|
||||||
|
|
||||||
const isEditingAllowed = allowPermissions(
|
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 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(
|
const canEditProperties = useCallback(
|
||||||
(projectId: string | undefined) => {
|
(projectId: string | undefined) => {
|
||||||
const isEditingAllowedBasedOnProject =
|
const isEditingAllowedBasedOnProject =
|
||||||
|
|
@ -305,7 +319,9 @@ export const BaseKanBanRoot = observer(function BaseKanBanRoot(props: IBaseKanBa
|
||||||
addIssuesToView={addIssuesToView}
|
addIssuesToView={addIssuesToView}
|
||||||
cardVariant={cardVariant}
|
cardVariant={cardVariant}
|
||||||
scrollableContainerRef={scrollableContainerRef}
|
scrollableContainerRef={scrollableContainerRef}
|
||||||
handleOnDrop={handleOnDrop}
|
handleOnDrop={handleIssueDrop}
|
||||||
|
selectedIssueId={selectedIssueId}
|
||||||
|
onClearSelectedIssue={handleClearSelectedIssue}
|
||||||
loadMoreIssues={fetchMoreIssues}
|
loadMoreIssues={fetchMoreIssues}
|
||||||
isEpic={isEpic}
|
isEpic={isEpic}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,8 @@ interface IssueBlockProps {
|
||||||
shouldRenderByDefault?: boolean;
|
shouldRenderByDefault?: boolean;
|
||||||
isEpic?: boolean;
|
isEpic?: boolean;
|
||||||
cardVariant?: TKanbanCardVariant;
|
cardVariant?: TKanbanCardVariant;
|
||||||
|
selectedIssueId?: string;
|
||||||
|
onClearSelectedIssue?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IssueDetailsBlockProps {
|
interface IssueDetailsBlockProps {
|
||||||
|
|
@ -198,6 +200,8 @@ export const KanbanIssueBlock = observer(function KanbanIssueBlock(props: IssueB
|
||||||
shouldRenderByDefault,
|
shouldRenderByDefault,
|
||||||
isEpic = false,
|
isEpic = false,
|
||||||
cardVariant = "default",
|
cardVariant = "default",
|
||||||
|
selectedIssueId,
|
||||||
|
onClearSelectedIssue,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const cardRef = useRef<HTMLAnchorElement | null>(null);
|
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 isDragAllowed = canDragIssuesInCurrentGrouping && !issue?.tempId && canEditIssueProperties;
|
||||||
const projectIdentifier = getProjectIdentifierById(issue?.project_id);
|
const projectIdentifier = getProjectIdentifierById(issue?.project_id);
|
||||||
const isPeeked = issue ? getIsIssuePeeked(issue.id) : false;
|
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({
|
const workItemLink = generateWorkItemLink({
|
||||||
workspaceSlug,
|
workspaceSlug,
|
||||||
|
|
@ -318,7 +330,7 @@ export const KanbanIssueBlock = observer(function KanbanIssueBlock(props: IssueB
|
||||||
{ "z-[100]": isCurrentBlockDragging && cardVariant === "internal-contour" }
|
{ "z-[100]": isCurrentBlockDragging && cardVariant === "internal-contour" }
|
||||||
)}
|
)}
|
||||||
data-card-variant={cardVariant}
|
data-card-variant={cardVariant}
|
||||||
onClick={() => handleIssuePeekOverview(issue)}
|
onClick={handleIssueBlockClick}
|
||||||
disabled={!!issue?.tempId}
|
disabled={!!issue?.tempId}
|
||||||
>
|
>
|
||||||
<RenderIfVisible
|
<RenderIfVisible
|
||||||
|
|
@ -337,7 +349,7 @@ export const KanbanIssueBlock = observer(function KanbanIssueBlock(props: IssueB
|
||||||
quickActions={quickActions}
|
quickActions={quickActions}
|
||||||
isReadOnly={!canEditIssueProperties}
|
isReadOnly={!canEditIssueProperties}
|
||||||
isEpic={isEpic}
|
isEpic={isEpic}
|
||||||
isActive={isPeeked || (cardVariant === "internal-contour" && isCurrentBlockDragging)}
|
isActive={isPeeked || isDropSelected || (cardVariant === "internal-contour" && isCurrentBlockDragging)}
|
||||||
cardVariant={cardVariant}
|
cardVariant={cardVariant}
|
||||||
/>
|
/>
|
||||||
</RenderIfVisible>
|
</RenderIfVisible>
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ interface IssueBlocksListProps {
|
||||||
quickActions: TRenderQuickActions;
|
quickActions: TRenderQuickActions;
|
||||||
canEditProperties: (projectId: string | undefined) => boolean;
|
canEditProperties: (projectId: string | undefined) => boolean;
|
||||||
cardVariant?: TKanbanCardVariant;
|
cardVariant?: TKanbanCardVariant;
|
||||||
|
selectedIssueId?: string;
|
||||||
|
onClearSelectedIssue?: () => void;
|
||||||
canDropOverIssue: boolean;
|
canDropOverIssue: boolean;
|
||||||
canDragIssuesInCurrentGrouping: boolean;
|
canDragIssuesInCurrentGrouping: boolean;
|
||||||
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
||||||
|
|
@ -42,6 +44,8 @@ export const KanbanIssueBlocksList = observer(function KanbanIssueBlocksList(pro
|
||||||
quickActions,
|
quickActions,
|
||||||
canEditProperties,
|
canEditProperties,
|
||||||
cardVariant = "default",
|
cardVariant = "default",
|
||||||
|
selectedIssueId,
|
||||||
|
onClearSelectedIssue,
|
||||||
scrollableContainerRef,
|
scrollableContainerRef,
|
||||||
isEpic = false,
|
isEpic = false,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
@ -70,6 +74,8 @@ export const KanbanIssueBlocksList = observer(function KanbanIssueBlocksList(pro
|
||||||
quickActions={quickActions}
|
quickActions={quickActions}
|
||||||
draggableId={draggableId}
|
draggableId={draggableId}
|
||||||
cardVariant={cardVariant}
|
cardVariant={cardVariant}
|
||||||
|
selectedIssueId={selectedIssueId}
|
||||||
|
onClearSelectedIssue={onClearSelectedIssue}
|
||||||
canDropOverIssue={canDropOverIssue}
|
canDropOverIssue={canDropOverIssue}
|
||||||
canDragIssuesInCurrentGrouping={canDragIssuesInCurrentGrouping}
|
canDragIssuesInCurrentGrouping={canDragIssuesInCurrentGrouping}
|
||||||
canEditProperties={canEditProperties}
|
canEditProperties={canEditProperties}
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,8 @@ export interface IKanBan {
|
||||||
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
|
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
|
||||||
canEditProperties: (projectId: string | undefined) => boolean;
|
canEditProperties: (projectId: string | undefined) => boolean;
|
||||||
cardVariant?: TKanbanCardVariant;
|
cardVariant?: TKanbanCardVariant;
|
||||||
|
selectedIssueId?: string;
|
||||||
|
onClearSelectedIssue?: () => void;
|
||||||
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
||||||
handleOnDrop: (source: GroupDropLocation, destination: GroupDropLocation) => Promise<void>;
|
handleOnDrop: (source: GroupDropLocation, destination: GroupDropLocation) => Promise<void>;
|
||||||
showEmptyGroup?: boolean;
|
showEmptyGroup?: boolean;
|
||||||
|
|
@ -91,6 +93,8 @@ export const KanBan = observer(function KanBan(props: IKanBan) {
|
||||||
addIssuesToView,
|
addIssuesToView,
|
||||||
canEditProperties,
|
canEditProperties,
|
||||||
cardVariant = "default",
|
cardVariant = "default",
|
||||||
|
selectedIssueId,
|
||||||
|
onClearSelectedIssue,
|
||||||
scrollableContainerRef,
|
scrollableContainerRef,
|
||||||
handleOnDrop,
|
handleOnDrop,
|
||||||
showEmptyGroup = true,
|
showEmptyGroup = true,
|
||||||
|
|
@ -228,6 +232,8 @@ export const KanBan = observer(function KanBan(props: IKanBan) {
|
||||||
disableIssueCreation={disableIssueCreation}
|
disableIssueCreation={disableIssueCreation}
|
||||||
canEditProperties={canEditProperties}
|
canEditProperties={canEditProperties}
|
||||||
cardVariant={cardVariant}
|
cardVariant={cardVariant}
|
||||||
|
selectedIssueId={selectedIssueId}
|
||||||
|
onClearSelectedIssue={onClearSelectedIssue}
|
||||||
scrollableContainerRef={scrollableContainerRef}
|
scrollableContainerRef={scrollableContainerRef}
|
||||||
loadMoreIssues={loadMoreIssues}
|
loadMoreIssues={loadMoreIssues}
|
||||||
handleOnDrop={handleOnDrop}
|
handleOnDrop={handleOnDrop}
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,8 @@ interface IKanbanGroup {
|
||||||
disableIssueCreation?: boolean;
|
disableIssueCreation?: boolean;
|
||||||
canEditProperties: (projectId: string | undefined) => boolean;
|
canEditProperties: (projectId: string | undefined) => boolean;
|
||||||
cardVariant?: TKanbanCardVariant;
|
cardVariant?: TKanbanCardVariant;
|
||||||
|
selectedIssueId?: string;
|
||||||
|
onClearSelectedIssue?: () => void;
|
||||||
groupByVisibilityToggle?: boolean;
|
groupByVisibilityToggle?: boolean;
|
||||||
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
||||||
handleOnDrop: (source: GroupDropLocation, destination: GroupDropLocation) => Promise<void>;
|
handleOnDrop: (source: GroupDropLocation, destination: GroupDropLocation) => Promise<void>;
|
||||||
|
|
@ -90,6 +92,8 @@ export const KanbanGroup = observer(function KanbanGroup(props: IKanbanGroup) {
|
||||||
quickActions,
|
quickActions,
|
||||||
canEditProperties,
|
canEditProperties,
|
||||||
cardVariant = "default",
|
cardVariant = "default",
|
||||||
|
selectedIssueId,
|
||||||
|
onClearSelectedIssue,
|
||||||
loadMoreIssues,
|
loadMoreIssues,
|
||||||
enableQuickIssueCreate,
|
enableQuickIssueCreate,
|
||||||
disableIssueCreation,
|
disableIssueCreation,
|
||||||
|
|
@ -314,6 +318,8 @@ export const KanbanGroup = observer(function KanbanGroup(props: IKanbanGroup) {
|
||||||
quickActions={quickActions}
|
quickActions={quickActions}
|
||||||
canEditProperties={canEditProperties}
|
canEditProperties={canEditProperties}
|
||||||
cardVariant={cardVariant}
|
cardVariant={cardVariant}
|
||||||
|
selectedIssueId={selectedIssueId}
|
||||||
|
onClearSelectedIssue={onClearSelectedIssue}
|
||||||
scrollableContainerRef={scrollableContainerRef}
|
scrollableContainerRef={scrollableContainerRef}
|
||||||
canDropOverIssue={!canOverlayBeVisible}
|
canDropOverIssue={!canOverlayBeVisible}
|
||||||
canDragIssuesInCurrentGrouping={canDragIssuesInCurrentGrouping}
|
canDragIssuesInCurrentGrouping={canDragIssuesInCurrentGrouping}
|
||||||
|
|
|
||||||
|
|
@ -128,6 +128,8 @@ interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader {
|
||||||
quickActions: TRenderQuickActions;
|
quickActions: TRenderQuickActions;
|
||||||
quickAddCallback?: (projectId: string | null | undefined, data: TIssue) => Promise<TIssue | undefined>;
|
quickAddCallback?: (projectId: string | null | undefined, data: TIssue) => Promise<TIssue | undefined>;
|
||||||
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
||||||
|
selectedIssueId?: string;
|
||||||
|
onClearSelectedIssue?: () => void;
|
||||||
showEmptyGroup: boolean;
|
showEmptyGroup: boolean;
|
||||||
updateIssue: ((projectId: string | null, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined;
|
updateIssue: ((projectId: string | null, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined;
|
||||||
}
|
}
|
||||||
|
|
@ -154,6 +156,8 @@ const SubGroupSwimlane = observer(function SubGroupSwimlane(props: ISubGroupSwim
|
||||||
quickActions,
|
quickActions,
|
||||||
quickAddCallback,
|
quickAddCallback,
|
||||||
scrollableContainerRef,
|
scrollableContainerRef,
|
||||||
|
selectedIssueId,
|
||||||
|
onClearSelectedIssue,
|
||||||
showEmptyGroup,
|
showEmptyGroup,
|
||||||
sub_group_by,
|
sub_group_by,
|
||||||
updateIssue,
|
updateIssue,
|
||||||
|
|
@ -223,6 +227,8 @@ const SubGroupSwimlane = observer(function SubGroupSwimlane(props: ISubGroupSwim
|
||||||
addIssuesToView={addIssuesToView}
|
addIssuesToView={addIssuesToView}
|
||||||
quickAddCallback={quickAddCallback}
|
quickAddCallback={quickAddCallback}
|
||||||
scrollableContainerRef={scrollableContainerRef}
|
scrollableContainerRef={scrollableContainerRef}
|
||||||
|
selectedIssueId={selectedIssueId}
|
||||||
|
onClearSelectedIssue={onClearSelectedIssue}
|
||||||
loadMoreIssues={loadMoreIssues}
|
loadMoreIssues={loadMoreIssues}
|
||||||
handleOnDrop={handleOnDrop}
|
handleOnDrop={handleOnDrop}
|
||||||
orderBy={orderBy}
|
orderBy={orderBy}
|
||||||
|
|
@ -263,6 +269,8 @@ export interface IKanBanSwimLanes {
|
||||||
quickActions: TRenderQuickActions;
|
quickActions: TRenderQuickActions;
|
||||||
quickAddCallback?: (projectId: string | null | undefined, data: TIssue) => Promise<TIssue | undefined>;
|
quickAddCallback?: (projectId: string | null | undefined, data: TIssue) => Promise<TIssue | undefined>;
|
||||||
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
||||||
|
selectedIssueId?: string;
|
||||||
|
onClearSelectedIssue?: () => void;
|
||||||
showEmptyGroup: boolean;
|
showEmptyGroup: boolean;
|
||||||
sub_group_by: TIssueGroupByOptions | undefined;
|
sub_group_by: TIssueGroupByOptions | undefined;
|
||||||
updateIssue: ((projectId: string | null, issueId: string, data: Partial<TIssue>) => Promise<void>) | 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,
|
addIssuesToView,
|
||||||
quickAddCallback,
|
quickAddCallback,
|
||||||
scrollableContainerRef,
|
scrollableContainerRef,
|
||||||
|
selectedIssueId,
|
||||||
|
onClearSelectedIssue,
|
||||||
isEpic = false,
|
isEpic = false,
|
||||||
} = props;
|
} = props;
|
||||||
// store hooks
|
// store hooks
|
||||||
|
|
@ -350,6 +360,8 @@ export const KanBanSwimLanes = observer(function KanBanSwimLanes(props: IKanBanS
|
||||||
cardVariant={cardVariant}
|
cardVariant={cardVariant}
|
||||||
quickAddCallback={quickAddCallback}
|
quickAddCallback={quickAddCallback}
|
||||||
scrollableContainerRef={scrollableContainerRef}
|
scrollableContainerRef={scrollableContainerRef}
|
||||||
|
selectedIssueId={selectedIssueId}
|
||||||
|
onClearSelectedIssue={onClearSelectedIssue}
|
||||||
isEpic={isEpic}
|
isEpic={isEpic}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -355,12 +355,15 @@ export const highlightIssueOnDrop = (
|
||||||
) => {
|
) => {
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
const sourceElementId = elementId ?? "";
|
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") {
|
if (sourceElement?.dataset.cardVariant === "internal-contour") {
|
||||||
sourceElement.classList.remove(HIGHLIGHT_CLASS, HIGHLIGHT_WITH_LINE);
|
sourceElement.classList.remove(HIGHLIGHT_CLASS, HIGHLIGHT_WITH_LINE, NODEDC_DROP_FILL_HIGHLIGHT_CLASS);
|
||||||
sourceElement.classList.add(NODEDC_DROP_FILL_HIGHLIGHT_CLASS);
|
|
||||||
window.setTimeout(() => sourceElement.classList.remove(NODEDC_DROP_FILL_HIGHLIGHT_CLASS), 1600);
|
|
||||||
|
|
||||||
if (shouldScrollIntoView)
|
if (shouldScrollIntoView)
|
||||||
await scrollIntoView(sourceElement, { behavior: "smooth", block: "center", duration: 1500 });
|
await scrollIntoView(sourceElement, { behavior: "smooth", block: "center", duration: 1500 });
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue