mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
[WEB-1580] chore: drag handler and sidebar improvement (#4780)
* chore: sidebar width reduced * chore: drag handler added to project sidebar and ui improvement * chore: label drag handler added
This commit is contained in:
parent
64619bf5eb
commit
7677f021a9
@ -32,10 +32,9 @@ export const AppSidebar: FC<IAppSidebar> = observer(() => {
|
|||||||
<div
|
<div
|
||||||
className={`fixed inset-y-0 z-20 flex h-full flex-shrink-0 flex-grow-0 flex-col border-r border-custom-sidebar-border-200 bg-custom-sidebar-background-100
|
className={`fixed inset-y-0 z-20 flex h-full flex-shrink-0 flex-grow-0 flex-col border-r border-custom-sidebar-border-200 bg-custom-sidebar-background-100
|
||||||
duration-300 md:relative
|
duration-300 md:relative
|
||||||
${sidebarCollapsed ? "-ml-[280px]" : ""}
|
${sidebarCollapsed ? "-ml-[250px]" : ""}
|
||||||
sm:${sidebarCollapsed ? "-ml-[280px]" : ""}
|
sm:${sidebarCollapsed ? "-ml-[250px]" : ""}
|
||||||
md:ml-0 ${sidebarCollapsed ? "w-[80px]" : "w-[280px]"}
|
md:ml-0 ${sidebarCollapsed ? "w-[70px]" : "w-[250px]"}
|
||||||
lg:ml-0 ${sidebarCollapsed ? "w-[80px]" : "w-[280px]"}
|
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<div ref={ref} className="flex h-full w-full flex-1 flex-col">
|
<div ref={ref} className="flex h-full w-full flex-1 flex-col">
|
||||||
|
@ -95,10 +95,9 @@ export const ProfileLayoutSidebar = observer(() => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`fixed inset-y-0 z-20 flex h-full flex-shrink-0 flex-grow-0 flex-col border-r border-custom-sidebar-border-200 bg-custom-sidebar-background-100 duration-300 md:relative
|
className={`fixed inset-y-0 z-20 flex h-full flex-shrink-0 flex-grow-0 flex-col border-r border-custom-sidebar-border-200 bg-custom-sidebar-background-100 duration-300 md:relative
|
||||||
${sidebarCollapsed ? "-ml-[280px]" : ""}
|
${sidebarCollapsed ? "-ml-[250px]" : ""}
|
||||||
sm:${sidebarCollapsed ? "-ml-[280px]" : ""}
|
sm:${sidebarCollapsed ? "-ml-[250px]" : ""}
|
||||||
md:ml-0 ${sidebarCollapsed ? "w-[80px]" : "w-[280px]"}
|
md:ml-0 ${sidebarCollapsed ? "w-[70px]" : "w-[250px]"}
|
||||||
lg:ml-0 ${sidebarCollapsed ? "w-[80px]" : "w-[280px]"}
|
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<div ref={ref} className="flex h-full w-full flex-col gap-y-4">
|
<div ref={ref} className="flex h-full w-full flex-col gap-y-4">
|
||||||
|
@ -27,10 +27,11 @@ interface ILabelItemBlock {
|
|||||||
customMenuItems: ICustomMenuItem[];
|
customMenuItems: ICustomMenuItem[];
|
||||||
handleLabelDelete: (label: IIssueLabel) => void;
|
handleLabelDelete: (label: IIssueLabel) => void;
|
||||||
isLabelGroup?: boolean;
|
isLabelGroup?: boolean;
|
||||||
|
dragHandleRef: MutableRefObject<HTMLButtonElement | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LabelItemBlock = (props: ILabelItemBlock) => {
|
export const LabelItemBlock = (props: ILabelItemBlock) => {
|
||||||
const { label, customMenuItems, handleLabelDelete, isLabelGroup } = props;
|
const { label, isDragging, customMenuItems, handleLabelDelete, isLabelGroup, dragHandleRef } = props;
|
||||||
// states
|
// states
|
||||||
const [isMenuActive, setIsMenuActive] = useState(false);
|
const [isMenuActive, setIsMenuActive] = useState(false);
|
||||||
// refs
|
// refs
|
||||||
@ -41,6 +42,12 @@ export const LabelItemBlock = (props: ILabelItemBlock) => {
|
|||||||
return (
|
return (
|
||||||
<div className="group flex items-center">
|
<div className="group flex items-center">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
|
<DragHandle
|
||||||
|
className={cn("opacity-0 group-hover:opacity-100", {
|
||||||
|
"opacity-100": isDragging,
|
||||||
|
})}
|
||||||
|
ref={dragHandleRef}
|
||||||
|
/>
|
||||||
<LabelName color={label.color} name={label.name} isGroup={isLabelGroup ?? false} />
|
<LabelName color={label.color} name={label.name} isGroup={isLabelGroup ?? false} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ export const LabelName = (props: ILabelName) => {
|
|||||||
const { name, color, isGroup } = props;
|
const { name, color, isGroup } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-3 pl-2">
|
<div className="flex items-center gap-3">
|
||||||
{isGroup ? (
|
{isGroup ? (
|
||||||
<Component className="h-3.5 w-3.5" color={color} />
|
<Component className="h-3.5 w-3.5" color={color} />
|
||||||
) : (
|
) : (
|
||||||
|
@ -55,13 +55,13 @@ export const ProjectSettingLabelGroup: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<LabelDndHOC label={label} isGroup isChild={false} isLastChild={isLastChild} onDrop={onDrop}>
|
<LabelDndHOC label={label} isGroup isChild={false} isLastChild={isLastChild} onDrop={onDrop}>
|
||||||
{(isDragging, isDroppingInLabel) => (
|
{(isDragging, isDroppingInLabel, dragHandleRef) => (
|
||||||
<div
|
<div
|
||||||
className={`rounded ${isDroppingInLabel ? "border-[2px] border-custom-primary-100" : "border-[1.5px] border-transparent"}`}
|
className={`rounded ${isDroppingInLabel ? "border-[2px] border-custom-primary-100" : "border-[1.5px] border-transparent"}`}
|
||||||
>
|
>
|
||||||
<Disclosure
|
<Disclosure
|
||||||
as="div"
|
as="div"
|
||||||
className={`rounded text-custom-text-100 cursor-grab ${
|
className={`rounded text-custom-text-100 ${
|
||||||
!isDroppingInLabel ? "border-[0.5px] border-custom-border-200" : ""
|
!isDroppingInLabel ? "border-[0.5px] border-custom-border-200" : ""
|
||||||
} ${isDragging ? "bg-custom-background-80" : "bg-custom-background-100"}`}
|
} ${isDragging ? "bg-custom-background-80" : "bg-custom-background-100"}`}
|
||||||
defaultOpen
|
defaultOpen
|
||||||
@ -89,6 +89,7 @@ export const ProjectSettingLabelGroup: React.FC<Props> = observer((props) => {
|
|||||||
customMenuItems={customMenuItems}
|
customMenuItems={customMenuItems}
|
||||||
handleLabelDelete={handleLabelDelete}
|
handleLabelDelete={handleLabelDelete}
|
||||||
isLabelGroup
|
isLabelGroup
|
||||||
|
dragHandleRef={dragHandleRef}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@ -64,9 +64,9 @@ export const ProjectSettingLabelItem: React.FC<Props> = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<LabelDndHOC label={label} isGroup={false} isChild={isChild} isLastChild={isLastChild} onDrop={onDrop}>
|
<LabelDndHOC label={label} isGroup={false} isChild={isChild} isLastChild={isLastChild} onDrop={onDrop}>
|
||||||
{(isDragging, isDroppingInLabel) => (
|
{(isDragging, isDroppingInLabel, dragHandleRef) => (
|
||||||
<div
|
<div
|
||||||
className={`rounded cursor-grab ${isDroppingInLabel ? "border-[2px] border-custom-primary-100" : "border-[1.5px] border-transparent"}`}
|
className={`rounded ${isDroppingInLabel ? "border-[2px] border-custom-primary-100" : "border-[1.5px] border-transparent"}`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`py-3 px-1 group relative flex items-center justify-between gap-2 space-y-3 rounded ${
|
className={`py-3 px-1 group relative flex items-center justify-between gap-2 space-y-3 rounded ${
|
||||||
@ -90,6 +90,7 @@ export const ProjectSettingLabelItem: React.FC<Props> = (props) => {
|
|||||||
isDragging={isDragging}
|
isDragging={isDragging}
|
||||||
customMenuItems={customMenuItems}
|
customMenuItems={customMenuItems}
|
||||||
handleLabelDelete={handleLabelDelete}
|
handleLabelDelete={handleLabelDelete}
|
||||||
|
dragHandleRef={dragHandleRef}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -12,6 +12,7 @@ import { useParams, usePathname } from "next/navigation";
|
|||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
// icons
|
// icons
|
||||||
import {
|
import {
|
||||||
|
MoreVertical,
|
||||||
PenSquare,
|
PenSquare,
|
||||||
LinkIcon,
|
LinkIcon,
|
||||||
Star,
|
Star,
|
||||||
@ -123,6 +124,7 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
|
|||||||
// refs
|
// refs
|
||||||
const actionSectionRef = useRef<HTMLDivElement | null>(null);
|
const actionSectionRef = useRef<HTMLDivElement | null>(null);
|
||||||
const projectRef = useRef<HTMLDivElement | null>(null);
|
const projectRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const dragHandleRef = useRef<HTMLButtonElement | null>(null);
|
||||||
// router params
|
// router params
|
||||||
const { workspaceSlug, projectId: URLProjectId } = useParams();
|
const { workspaceSlug, projectId: URLProjectId } = useParams();
|
||||||
// pathname
|
// pathname
|
||||||
@ -185,6 +187,7 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const element = projectRef.current;
|
const element = projectRef.current;
|
||||||
|
const dragHandleElement = dragHandleRef.current;
|
||||||
|
|
||||||
if (!element) return;
|
if (!element) return;
|
||||||
|
|
||||||
@ -192,6 +195,7 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
|
|||||||
draggable({
|
draggable({
|
||||||
element,
|
element,
|
||||||
canDrag: () => !disableDrag && !isSideBarCollapsed,
|
canDrag: () => !disableDrag && !isSideBarCollapsed,
|
||||||
|
dragHandle: dragHandleElement ?? undefined,
|
||||||
getInitialData: () => ({ id: projectId, dragInstanceId: "PROJECTS" }),
|
getInitialData: () => ({ id: projectId, dragInstanceId: "PROJECTS" }),
|
||||||
onDragStart: () => {
|
onDragStart: () => {
|
||||||
setIsDragging(true);
|
setIsDragging(true);
|
||||||
@ -207,7 +211,7 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
|
|||||||
const root = createRoot(container);
|
const root = createRoot(container);
|
||||||
root.render(
|
root.render(
|
||||||
<div className="rounded flex items-center bg-custom-background-100 text-sm p-1 pr-2">
|
<div className="rounded flex items-center bg-custom-background-100 text-sm p-1 pr-2">
|
||||||
<div className="h-6 w-6 grid place-items-center flex-shrink-0">
|
<div className="size-4 grid place-items-center flex-shrink-0">
|
||||||
{project && <Logo logo={project?.logo_props} />}
|
{project && <Logo logo={project?.logo_props} />}
|
||||||
</div>
|
</div>
|
||||||
<p className="truncate text-custom-sidebar-text-200">{project?.name}</p>
|
<p className="truncate text-custom-sidebar-text-200">{project?.name}</p>
|
||||||
@ -268,7 +272,7 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}, [projectRef?.current, projectId, isLastChild, projectListType, handleOnProjectDrop]);
|
}, [projectRef?.current, dragHandleRef?.current, projectId, isLastChild, projectListType, handleOnProjectDrop]);
|
||||||
|
|
||||||
useOutsideClickDetector(actionSectionRef, () => setIsMenuActive(false));
|
useOutsideClickDetector(actionSectionRef, () => setIsMenuActive(false));
|
||||||
useOutsideClickDetector(projectRef, () => projectRef?.current?.classList?.remove(HIGHLIGHT_CLASS));
|
useOutsideClickDetector(projectRef, () => projectRef?.current?.classList?.remove(HIGHLIGHT_CLASS));
|
||||||
@ -288,12 +292,39 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
|
|||||||
<DropIndicator classNames="absolute top-0" isVisible={instruction === "DRAG_OVER"} />
|
<DropIndicator classNames="absolute top-0" isVisible={instruction === "DRAG_OVER"} />
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"group relative flex w-full items-center rounded-md py-1 text-custom-sidebar-text-100 hover:bg-custom-sidebar-background-80",
|
"h-9 group relative flex w-full px-3 py-2 items-center rounded-md text-custom-sidebar-text-100 hover:bg-custom-sidebar-background-80",
|
||||||
{
|
{
|
||||||
"bg-custom-sidebar-background-80": isMenuActive,
|
"bg-custom-sidebar-background-80": isMenuActive,
|
||||||
|
"pr-0": !isSideBarCollapsed,
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
{!disableDrag && (
|
||||||
|
<Tooltip
|
||||||
|
isMobile={isMobile}
|
||||||
|
tooltipContent={project.sort_order === null ? "Join the project to rearrange" : "Drag to rearrange"}
|
||||||
|
position="top-right"
|
||||||
|
disabled={isDragging}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={cn(
|
||||||
|
"absolute -left-[18px] flex opacity-0 rounded text-custom-sidebar-text-400 ml-2 cursor-grab",
|
||||||
|
{
|
||||||
|
"group-hover:opacity-100": !isSideBarCollapsed,
|
||||||
|
"cursor-not-allowed opacity-60": project.sort_order === null,
|
||||||
|
"cursor-grabbing": isDragging,
|
||||||
|
flex: isMenuActive,
|
||||||
|
hidden: isSideBarCollapsed,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
ref={dragHandleRef}
|
||||||
|
>
|
||||||
|
<MoreVertical className="-ml-3 h-3.5" />
|
||||||
|
<MoreVertical className="-ml-5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
<Tooltip
|
<Tooltip
|
||||||
tooltipContent={`${project.name}`}
|
tooltipContent={`${project.name}`}
|
||||||
position="right"
|
position="right"
|
||||||
@ -303,18 +334,18 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
|
|||||||
<Disclosure.Button
|
<Disclosure.Button
|
||||||
as="div"
|
as="div"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-grow cursor-pointer select-none items-center justify-between truncate text-left text-sm font-medium pl-2",
|
"flex flex-grow cursor-pointer select-none items-center justify-between truncate text-left text-sm font-medium",
|
||||||
{
|
{
|
||||||
"justify-center": isSideBarCollapsed,
|
"justify-center": isSideBarCollapsed,
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={cn("flex w-full h-7 flex-grow items-center gap-1 truncate", {
|
className={cn("flex w-full flex-grow items-center gap-2.5 truncate", {
|
||||||
"justify-center": isSideBarCollapsed,
|
"justify-center": isSideBarCollapsed,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div className="h-5 w-5 grid place-items-center flex-shrink-0 mr-1">
|
<div className="size-5 grid place-items-center flex-shrink-0">
|
||||||
<Logo logo={project.logo_props} />
|
<Logo logo={project.logo_props} />
|
||||||
</div>
|
</div>
|
||||||
{!isSideBarCollapsed && <p className="truncate text-custom-sidebar-text-200">{project.name}</p>}
|
{!isSideBarCollapsed && <p className="truncate text-custom-sidebar-text-200">{project.name}</p>}
|
||||||
@ -322,7 +353,7 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
|
|||||||
{!isSideBarCollapsed && (
|
{!isSideBarCollapsed && (
|
||||||
<ChevronDown
|
<ChevronDown
|
||||||
className={cn(
|
className={cn(
|
||||||
"mb-0.5 hidden h-4 w-4 flex-shrink-0 text-custom-sidebar-text-400 duration-300 group-hover:block",
|
"hidden h-4 w-4 flex-shrink-0 text-custom-sidebar-text-400 duration-300 group-hover:block",
|
||||||
{
|
{
|
||||||
"rotate-180": open,
|
"rotate-180": open,
|
||||||
block: isMenuActive,
|
block: isMenuActive,
|
||||||
@ -338,17 +369,16 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
|
|||||||
customButton={
|
customButton={
|
||||||
<div
|
<div
|
||||||
ref={actionSectionRef}
|
ref={actionSectionRef}
|
||||||
className="my-auto mt-1.5 w-full cursor-pointer px-1 text-custom-sidebar-text-400 duration-300"
|
className="size-5 flex items-center justify-center cursor-pointer px-1 text-custom-sidebar-text-400 duration-300"
|
||||||
onClick={() => setIsMenuActive(!isMenuActive)}
|
onClick={() => setIsMenuActive(!isMenuActive)}
|
||||||
>
|
>
|
||||||
<MoreHorizontal className="h-3.5 w-3.5" />
|
<MoreHorizontal className="h-3.5 w-3.5" />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
className={cn("hidden flex-shrink-0 group-hover:block", {
|
className={cn("hidden flex-shrink-0 mr-1 group-hover:block", {
|
||||||
"!block": isMenuActive,
|
"!block": isMenuActive,
|
||||||
})}
|
})}
|
||||||
buttonClassName="!text-custom-sidebar-text-400"
|
buttonClassName="!text-custom-sidebar-text-400"
|
||||||
ellipsis
|
|
||||||
placement="bottom-start"
|
placement="bottom-start"
|
||||||
>
|
>
|
||||||
{!project.is_favorite && (
|
{!project.is_favorite && (
|
||||||
@ -432,7 +462,7 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
|
|||||||
leaveFrom="transform scale-100 opacity-100"
|
leaveFrom="transform scale-100 opacity-100"
|
||||||
leaveTo="transform scale-95 opacity-0"
|
leaveTo="transform scale-95 opacity-0"
|
||||||
>
|
>
|
||||||
<Disclosure.Panel className={`mt-1 space-y-1 ${isSideBarCollapsed ? "" : "ml-[2.25rem]"}`}>
|
<Disclosure.Panel className={`mt-1 space-y-1 ${isSideBarCollapsed ? "" : "ml-[1.25rem]"}`}>
|
||||||
{navigation(workspaceSlug as string, project?.id).map((item) => {
|
{navigation(workspaceSlug as string, project?.id).map((item) => {
|
||||||
if (
|
if (
|
||||||
(item.name === "Cycles" && !project.cycle_view) ||
|
(item.name === "Cycles" && !project.cycle_view) ||
|
||||||
|
@ -207,7 +207,7 @@ export const ProjectSidebarList: FC = observer(() => {
|
|||||||
leaveTo="transform scale-95 opacity-0"
|
leaveTo="transform scale-95 opacity-0"
|
||||||
>
|
>
|
||||||
{isFavoriteProjectsListOpen && (
|
{isFavoriteProjectsListOpen && (
|
||||||
<Disclosure.Panel as="div" className={`space-y-2 pl-2`} static>
|
<Disclosure.Panel as="div" static>
|
||||||
{favoriteProjects.map((projectId, index) => (
|
{favoriteProjects.map((projectId, index) => (
|
||||||
<ProjectSidebarListItem
|
<ProjectSidebarListItem
|
||||||
key={projectId}
|
key={projectId}
|
||||||
@ -269,7 +269,7 @@ export const ProjectSidebarList: FC = observer(() => {
|
|||||||
leaveTo="transform scale-95 opacity-0"
|
leaveTo="transform scale-95 opacity-0"
|
||||||
>
|
>
|
||||||
{isAllProjectsListOpen && (
|
{isAllProjectsListOpen && (
|
||||||
<Disclosure.Panel as="div" className="pl-2" static>
|
<Disclosure.Panel as="div" static>
|
||||||
{joinedProjects.map((projectId, index) => (
|
{joinedProjects.map((projectId, index) => (
|
||||||
<ProjectSidebarListItem
|
<ProjectSidebarListItem
|
||||||
key={projectId}
|
key={projectId}
|
||||||
|
Loading…
Reference in New Issue
Block a user