forked from github/plane
style: sidebar projects design (#1736)
* chore: disclosure menu for sidebar projects * fix: projects list spacing * style: new design
This commit is contained in:
parent
df8504e6f7
commit
cb4d294608
@ -5,6 +5,8 @@ import { mutate } from "swr";
|
||||
|
||||
// react-beautiful-dnd
|
||||
import { DragDropContext, Draggable, DropResult, Droppable } from "react-beautiful-dnd";
|
||||
// headless ui
|
||||
import { Disclosure } from "@headlessui/react";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
import useTheme from "hooks/use-theme";
|
||||
@ -15,6 +17,7 @@ import { DeleteProjectModal, SingleSidebarProject } from "components/project";
|
||||
// services
|
||||
import projectService from "services/project.service";
|
||||
// icons
|
||||
import { Icon } from "components/ui";
|
||||
import { PlusIcon } from "@heroicons/react/24/outline";
|
||||
// helpers
|
||||
import { copyTextToClipboard } from "helpers/string.helper";
|
||||
@ -30,7 +33,7 @@ export const ProjectSidebarList: FC = () => {
|
||||
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { user } = useUserAuth();
|
||||
|
||||
@ -38,15 +41,18 @@ export const ProjectSidebarList: FC = () => {
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const { projects: allProjects } = useProjects();
|
||||
|
||||
const joinedProjects = allProjects?.filter((p) => p.sort_order);
|
||||
const favoriteProjects = allProjects?.filter((p) => p.is_favorite);
|
||||
const otherProjects = allProjects?.filter((p) => p.sort_order === null);
|
||||
|
||||
const orderedAllProjects = allProjects
|
||||
? orderArrayBy(allProjects, "sort_order", "ascending")
|
||||
: [];
|
||||
const orderedJoinedProjects: IProject[] | undefined = joinedProjects
|
||||
? orderArrayBy(joinedProjects, "sort_order", "ascending")
|
||||
: undefined;
|
||||
|
||||
const orderedFavProjects = favoriteProjects
|
||||
const orderedFavProjects: IProject[] | undefined = favoriteProjects
|
||||
? orderArrayBy(favoriteProjects, "sort_order", "ascending")
|
||||
: [];
|
||||
: undefined;
|
||||
|
||||
const handleDeleteProject = (project: IProject) => {
|
||||
setProjectToDelete(project);
|
||||
@ -69,22 +75,25 @@ export const ProjectSidebarList: FC = () => {
|
||||
const { source, destination, draggableId } = result;
|
||||
|
||||
if (!destination || !workspaceSlug) return;
|
||||
|
||||
if (source.index === destination.index) return;
|
||||
|
||||
const projectList =
|
||||
destination.droppableId === "all-projects" ? orderedAllProjects : orderedFavProjects;
|
||||
const projectsList =
|
||||
(destination.droppableId === "joined-projects"
|
||||
? orderedJoinedProjects
|
||||
: orderedFavProjects) ?? [];
|
||||
|
||||
let updatedSortOrder = projectList[source.index].sort_order;
|
||||
if (destination.index === 0) {
|
||||
updatedSortOrder = projectList[0].sort_order - 1000;
|
||||
} else if (destination.index === projectList.length - 1) {
|
||||
updatedSortOrder = projectList[projectList.length - 1].sort_order + 1000;
|
||||
} else {
|
||||
const destinationSortingOrder = projectList[destination.index].sort_order;
|
||||
let updatedSortOrder = projectsList[source.index].sort_order;
|
||||
|
||||
if (destination.index === 0) updatedSortOrder = (projectsList[0].sort_order as number) - 1000;
|
||||
else if (destination.index === projectsList.length - 1)
|
||||
updatedSortOrder = (projectsList[projectsList.length - 1].sort_order as number) + 1000;
|
||||
else {
|
||||
const destinationSortingOrder = projectsList[destination.index].sort_order as number;
|
||||
const relativeDestinationSortingOrder =
|
||||
source.index < destination.index
|
||||
? projectList[destination.index + 1].sort_order
|
||||
: projectList[destination.index - 1].sort_order;
|
||||
? (projectsList[destination.index + 1].sort_order as number)
|
||||
: (projectsList[destination.index - 1].sort_order as number);
|
||||
|
||||
updatedSortOrder = Math.round(
|
||||
(destinationSortingOrder + relativeDestinationSortingOrder) / 2
|
||||
@ -121,20 +130,42 @@ export const ProjectSidebarList: FC = () => {
|
||||
data={projectToDelete}
|
||||
user={user}
|
||||
/>
|
||||
<div className="h-full overflow-y-auto px-4">
|
||||
<div className="h-full overflow-y-auto px-5 space-y-3 pt-3 border-t border-custom-sidebar-border-300">
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<Droppable droppableId="favorite-projects">
|
||||
{(provided) => (
|
||||
<div ref={provided.innerRef} {...provided.droppableProps}>
|
||||
{orderedFavProjects && orderedFavProjects.length > 0 && (
|
||||
<div className="flex flex-col space-y-2 mt-5">
|
||||
<Disclosure
|
||||
as="div"
|
||||
className="flex flex-col space-y-2"
|
||||
defaultOpen={
|
||||
projectId && orderedFavProjects.find((p) => p.id === projectId) ? true : false
|
||||
}
|
||||
>
|
||||
{({ open }) => (
|
||||
<>
|
||||
{!sidebarCollapse && (
|
||||
<h5 className="text-sm font-medium text-custom-sidebar-text-200">
|
||||
<Disclosure.Button
|
||||
as="button"
|
||||
type="button"
|
||||
className="group flex items-center gap-1 px-1.5 text-xs font-semibold text-custom-sidebar-text-200 text-left hover:bg-custom-sidebar-background-80 rounded w-min whitespace-nowrap"
|
||||
>
|
||||
Favorites
|
||||
</h5>
|
||||
<Icon
|
||||
iconName={open ? "arrow_drop_down" : "arrow_right"}
|
||||
className="group-hover:opacity-100 opacity-0 !text-lg"
|
||||
/>
|
||||
</Disclosure.Button>
|
||||
)}
|
||||
<Disclosure.Panel as="div" className="space-y-2">
|
||||
{orderedFavProjects.map((project, index) => (
|
||||
<Draggable key={project.id} draggableId={project.id} index={index}>
|
||||
<Draggable
|
||||
key={project.id}
|
||||
draggableId={project.id}
|
||||
index={index}
|
||||
isDragDisabled={project.sort_order === null}
|
||||
>
|
||||
{(provided, snapshot) => (
|
||||
<div ref={provided.innerRef} {...provided.draggableProps}>
|
||||
<SingleSidebarProject
|
||||
@ -151,23 +182,47 @@ export const ProjectSidebarList: FC = () => {
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
</Disclosure.Panel>
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Disclosure>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<Droppable droppableId="all-projects">
|
||||
<Droppable droppableId="joined-projects">
|
||||
{(provided) => (
|
||||
<div ref={provided.innerRef} {...provided.droppableProps}>
|
||||
{orderedAllProjects && orderedAllProjects.length > 0 && (
|
||||
<div className="flex flex-col space-y-2 mt-5">
|
||||
{orderedJoinedProjects && orderedJoinedProjects.length > 0 && (
|
||||
<Disclosure
|
||||
as="div"
|
||||
className="flex flex-col space-y-2"
|
||||
defaultOpen={
|
||||
projectId && orderedJoinedProjects.find((p) => p.id === projectId)
|
||||
? true
|
||||
: false
|
||||
}
|
||||
>
|
||||
{({ open }) => (
|
||||
<>
|
||||
{!sidebarCollapse && (
|
||||
<h5 className="text-sm font-medium text-custom-sidebar-text-200">Projects</h5>
|
||||
<Disclosure.Button
|
||||
as="button"
|
||||
type="button"
|
||||
className="group flex items-center gap-1 px-1.5 text-xs font-semibold text-custom-sidebar-text-200 text-left hover:bg-custom-sidebar-background-80 rounded w-min whitespace-nowrap"
|
||||
>
|
||||
Projects
|
||||
<Icon
|
||||
iconName={open ? "arrow_drop_down" : "arrow_right"}
|
||||
className="group-hover:opacity-100 opacity-0 !text-lg"
|
||||
/>
|
||||
</Disclosure.Button>
|
||||
)}
|
||||
{orderedAllProjects.map((project, index) => (
|
||||
<Disclosure.Panel as="div" className="space-y-2">
|
||||
{orderedJoinedProjects.map((project, index) => (
|
||||
<Draggable key={project.id} draggableId={project.id} index={index}>
|
||||
{(provided, snapshot) => (
|
||||
<div ref={provided.innerRef} {...provided.draggableProps}>
|
||||
@ -184,13 +239,53 @@ export const ProjectSidebarList: FC = () => {
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
</Disclosure.Panel>
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Disclosure>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
{otherProjects && otherProjects.length > 0 && (
|
||||
<Disclosure
|
||||
as="div"
|
||||
className="flex flex-col space-y-2"
|
||||
defaultOpen={projectId && otherProjects.find((p) => p.id === projectId) ? true : false}
|
||||
>
|
||||
{({ open }) => (
|
||||
<>
|
||||
{!sidebarCollapse && (
|
||||
<Disclosure.Button
|
||||
as="button"
|
||||
type="button"
|
||||
className="group flex items-center gap-1 px-1.5 text-xs font-semibold text-custom-sidebar-text-200 text-left hover:bg-custom-sidebar-background-80 rounded w-min whitespace-nowrap"
|
||||
>
|
||||
Other Projects
|
||||
<Icon
|
||||
iconName={open ? "arrow_drop_down" : "arrow_right"}
|
||||
className="group-hover:opacity-100 opacity-0 !text-lg"
|
||||
/>
|
||||
</Disclosure.Button>
|
||||
)}
|
||||
<Disclosure.Panel as="div" className="space-y-2">
|
||||
{otherProjects?.map((project, index) => (
|
||||
<SingleSidebarProject
|
||||
key={project.id}
|
||||
project={project}
|
||||
sidebarCollapse={sidebarCollapse}
|
||||
handleDeleteProject={() => handleDeleteProject(project)}
|
||||
handleCopyText={() => handleCopyText(project.id)}
|
||||
shortContextMenu
|
||||
/>
|
||||
))}
|
||||
</Disclosure.Panel>
|
||||
</>
|
||||
)}
|
||||
</Disclosure>
|
||||
)}
|
||||
{allProjects && allProjects.length === 0 && (
|
||||
<button
|
||||
type="button"
|
||||
|
@ -36,8 +36,8 @@ import { PROJECTS_LIST } from "constants/fetch-keys";
|
||||
type Props = {
|
||||
project: IProject;
|
||||
sidebarCollapse: boolean;
|
||||
provided: DraggableProvided;
|
||||
snapshot: DraggableStateSnapshot;
|
||||
provided?: DraggableProvided;
|
||||
snapshot?: DraggableStateSnapshot;
|
||||
handleDeleteProject: () => void;
|
||||
handleCopyText: () => void;
|
||||
shortContextMenu?: boolean;
|
||||
@ -133,34 +133,36 @@ export const SingleSidebarProject: React.FC<Props> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<Disclosure key={project?.id} defaultOpen={projectId === project?.id}>
|
||||
<Disclosure key={project.id} defaultOpen={projectId === project.id}>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<div
|
||||
className={`group relative flex items-center gap-x-1 text-custom-sidebar-text-100 ${
|
||||
snapshot.isDragging ? "opacity-60" : ""
|
||||
className={`group relative text-custom-sidebar-text-10 px-2 py-1 w-full flex items-center hover:bg-custom-sidebar-background-80 rounded-md ${
|
||||
snapshot?.isDragging ? "opacity-60" : ""
|
||||
}`}
|
||||
>
|
||||
{provided && (
|
||||
<button
|
||||
type="button"
|
||||
className={`absolute top-2 left-0 hidden rounded p-0.5 ${
|
||||
className={`absolute top-1/2 -translate-y-1/2 -left-4 hidden rounded p-0.5 ${
|
||||
sidebarCollapse ? "" : "group-hover:!flex"
|
||||
}`}
|
||||
{...provided.dragHandleProps}
|
||||
{...provided?.dragHandleProps}
|
||||
>
|
||||
<EllipsisVerticalIcon className="h-4" />
|
||||
<EllipsisVerticalIcon className="-ml-5 h-4" />
|
||||
</button>
|
||||
)}
|
||||
<Tooltip
|
||||
tooltipContent={`${project?.name}`}
|
||||
tooltipContent={`${project.name}`}
|
||||
position="right"
|
||||
className="ml-2"
|
||||
disabled={!sidebarCollapse}
|
||||
>
|
||||
<Disclosure.Button
|
||||
as="div"
|
||||
className={`flex w-full cursor-pointer select-none items-center rounded-sm py-1 text-left text-sm font-medium ${
|
||||
sidebarCollapse ? "justify-center" : "justify-between ml-4"
|
||||
className={`flex items-center w-full cursor-pointer select-none text-left text-sm font-medium ${
|
||||
sidebarCollapse ? "justify-center" : `justify-between`
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-x-2">
|
||||
@ -184,21 +186,23 @@ export const SingleSidebarProject: React.FC<Props> = ({
|
||||
open ? "" : "text-custom-sidebar-text-200"
|
||||
}`}
|
||||
>
|
||||
{truncateText(project?.name, 14)}
|
||||
{truncateText(project.name, 15)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{!sidebarCollapse && (
|
||||
<ExpandMoreOutlined
|
||||
fontSize="small"
|
||||
className={`${open ? "rotate-180" : ""} text-custom-text-200 duration-300`}
|
||||
className={`${
|
||||
open ? "rotate-180" : ""
|
||||
} !hidden group-hover:!block text-custom-sidebar-text-200 duration-300`}
|
||||
/>
|
||||
)}
|
||||
</Disclosure.Button>
|
||||
</Tooltip>
|
||||
|
||||
{!sidebarCollapse && (
|
||||
<CustomMenu ellipsis>
|
||||
<CustomMenu className="hidden group-hover:block" ellipsis>
|
||||
{!shortContextMenu && (
|
||||
<CustomMenu.MenuItem onClick={handleDeleteProject}>
|
||||
<span className="flex items-center justify-start gap-2 ">
|
||||
|
@ -47,7 +47,7 @@ export const WorkspaceSidebarMenu = () => {
|
||||
const { collapsed: sidebarCollapse } = useTheme();
|
||||
|
||||
return (
|
||||
<div className="w-full cursor-pointer space-y-2 px-4 mt-5">
|
||||
<div className="w-full cursor-pointer space-y-2 px-4 mt-5 pb-5">
|
||||
{workspaceLinks(workspaceSlug as string).map((link, index) => {
|
||||
const isActive =
|
||||
link.name === "Settings"
|
||||
|
@ -58,8 +58,6 @@ const useProfileIssues = (workspaceSlug: string | undefined, userId: string | un
|
||||
: null
|
||||
);
|
||||
|
||||
console.log(memberRole);
|
||||
|
||||
const groupedIssues:
|
||||
| {
|
||||
[key: string]: IIssue[];
|
||||
|
@ -18,7 +18,6 @@ export default function useUser({ redirectTo = "", redirectIfFound = false, opti
|
||||
);
|
||||
|
||||
const user = error ? undefined : data;
|
||||
// console.log("useUser", user);
|
||||
|
||||
useEffect(() => {
|
||||
// if no redirect needed, just return (example: already on /dashboard)
|
||||
|
2
apps/app/types/projects.d.ts
vendored
2
apps/app/types/projects.d.ts
vendored
@ -45,7 +45,7 @@ export interface IProject {
|
||||
network: number;
|
||||
page_view: boolean;
|
||||
project_lead: IUserLite | string | null;
|
||||
sort_order: number;
|
||||
sort_order: number | null;
|
||||
slug: string;
|
||||
total_cycles: number;
|
||||
total_members: number;
|
||||
|
Loading…
Reference in New Issue
Block a user