[WEB-3292] feat: workspace switcher redesign (#6543)
* feat: ui changes for workspace switcher * fix: hover * fix: added current plan * feat: Return user role * chore: remove unused imports * fix: css * fix: added user role in workspace switcher * fix: return role as integer * fix: role casing * fix: refactor * fix: plan pill fix * fix: design updates * fix: refactor * fix: member translation * fix: css improvements * fix: truncate issue * fix: workspace switcher dropdown email truncate * fix: workspace switcher dropdown email truncate * fix: role --------- Co-authored-by: sangeethailango <sangeethailango21@gmail.com> Co-authored-by: Anmol Singh Bhatia <anmolsinghbhatia@plane.so>
This commit is contained in:
parent
456c7f55a9
commit
20ba91b98c
7 changed files with 172 additions and 112 deletions
|
|
@ -32,10 +32,10 @@ from django.core.exceptions import ValidationError
|
|||
|
||||
|
||||
class WorkSpaceSerializer(DynamicBaseSerializer):
|
||||
owner = UserLiteSerializer(read_only=True)
|
||||
total_members = serializers.IntegerField(read_only=True)
|
||||
total_issues = serializers.IntegerField(read_only=True)
|
||||
logo_url = serializers.CharField(read_only=True)
|
||||
role = serializers.IntegerField(read_only=True)
|
||||
|
||||
def validate_slug(self, value):
|
||||
# Check if the slug is restricted
|
||||
|
|
|
|||
|
|
@ -7,9 +7,11 @@ from datetime import date
|
|||
from dateutil.relativedelta import relativedelta
|
||||
from django.db import IntegrityError
|
||||
from django.db.models import Count, F, Func, OuterRef, Prefetch, Q
|
||||
|
||||
from django.db.models.fields import DateField
|
||||
from django.db.models.functions import Cast, ExtractDay, ExtractWeek
|
||||
|
||||
|
||||
# Django imports
|
||||
from django.http import HttpResponse
|
||||
from django.utils import timezone
|
||||
|
|
@ -173,6 +175,11 @@ class UserWorkSpacesEndpoint(BaseAPIView):
|
|||
.values("count")
|
||||
)
|
||||
|
||||
role = (
|
||||
WorkspaceMember.objects.filter(workspace=OuterRef("id"), member=request.user, is_active=True)
|
||||
.values("role")
|
||||
)
|
||||
|
||||
workspace = (
|
||||
Workspace.objects.prefetch_related(
|
||||
Prefetch(
|
||||
|
|
@ -184,17 +191,19 @@ class UserWorkSpacesEndpoint(BaseAPIView):
|
|||
)
|
||||
.select_related("owner")
|
||||
.annotate(total_members=member_count)
|
||||
.annotate(total_issues=issue_count)
|
||||
.annotate(total_issues=issue_count, role=role)
|
||||
.filter(
|
||||
workspace_member__member=request.user, workspace_member__is_active=True
|
||||
)
|
||||
.distinct()
|
||||
)
|
||||
|
||||
workspaces = WorkSpaceSerializer(
|
||||
self.filter_queryset(workspace),
|
||||
fields=fields if fields else None,
|
||||
many=True,
|
||||
).data
|
||||
|
||||
return Response(workspaces, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
|
|
|
|||
2
packages/types/src/workspace.d.ts
vendored
2
packages/types/src/workspace.d.ts
vendored
|
|
@ -23,6 +23,8 @@ export interface IWorkspace {
|
|||
organization_size: string;
|
||||
total_issues: number;
|
||||
total_projects?: number;
|
||||
current_plan?: string;
|
||||
role: number;
|
||||
}
|
||||
|
||||
export interface IWorkspaceLite {
|
||||
|
|
|
|||
7
web/ce/components/common/subscription-pill.tsx
Normal file
7
web/ce/components/common/subscription-pill.tsx
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { IWorkspace } from "@plane/types";
|
||||
|
||||
type TProps = {
|
||||
workspace: IWorkspace;
|
||||
};
|
||||
|
||||
export const SubscriptionPill = (props: TProps) => <></>;
|
||||
106
web/core/components/workspace/sidebar/dropdown-item.tsx
Normal file
106
web/core/components/workspace/sidebar/dropdown-item.tsx
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
import { Check, Settings, UserPlus } from "lucide-react";
|
||||
import { Menu } from "@headlessui/react";
|
||||
import { EUserPermissions } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { IWorkspace } from "@plane/types";
|
||||
import { cn, getFileURL } from "@plane/utils";
|
||||
import { getUserRole } from "@/helpers/user.helper";
|
||||
import { SubscriptionPill } from "@/plane-web/components/common/subscription-pill";
|
||||
|
||||
type TProps = {
|
||||
workspace: IWorkspace;
|
||||
activeWorkspace: IWorkspace | null;
|
||||
handleItemClick: () => void;
|
||||
handleWorkspaceNavigation: (workspace: IWorkspace) => void;
|
||||
};
|
||||
const SidebarDropdownItem = (props: TProps) => {
|
||||
const { workspace, activeWorkspace, handleItemClick, handleWorkspaceNavigation } = props;
|
||||
|
||||
// router params
|
||||
const { workspaceSlug } = useParams();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={workspace.id}
|
||||
href={`/${workspace.slug}`}
|
||||
onClick={() => {
|
||||
handleWorkspaceNavigation(workspace);
|
||||
handleItemClick();
|
||||
}}
|
||||
className="w-full"
|
||||
id={workspace.id}
|
||||
>
|
||||
<Menu.Item
|
||||
as="div"
|
||||
className={cn("px-4 py-2", {
|
||||
"bg-custom-sidebar-background-90": workspace.id === activeWorkspace?.id,
|
||||
"hover:bg-custom-sidebar-background-90": workspace.id !== activeWorkspace?.id,
|
||||
})}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-1 rounded p-1 text-sm text-custom-sidebar-text-100 ">
|
||||
<div className="flex items-center justify-start gap-2.5 w-[80%] relative">
|
||||
<span
|
||||
className={`relative flex h-8 w-8 flex-shrink-0 items-center justify-center p-2 text-sm uppercase font-semibold ${
|
||||
!workspace?.logo_url && "rounded-lg bg-custom-primary-500 text-white"
|
||||
}`}
|
||||
>
|
||||
{workspace?.logo_url && workspace.logo_url !== "" ? (
|
||||
<img
|
||||
src={getFileURL(workspace.logo_url)}
|
||||
className="absolute left-0 top-0 h-full w-full rounded-lg object-cover"
|
||||
alt={t("workspace_logo")}
|
||||
/>
|
||||
) : (
|
||||
(workspace?.name?.[0] ?? "...")
|
||||
)}
|
||||
</span>
|
||||
<div className="w-[inherit]">
|
||||
<div
|
||||
className={`truncate text-ellipsis text-sm font-medium ${workspaceSlug === workspace.slug ? "" : "text-custom-text-200"}`}
|
||||
>
|
||||
{workspace.name}
|
||||
</div>
|
||||
<div className="text-sm text-custom-text-300 flex gap-2 capitalize w-fit">
|
||||
<span>{getUserRole(workspace.role)?.toLowerCase() || "guest"}</span>
|
||||
<div className="w-1 h-1 bg-custom-text-300/50 rounded-full m-auto" />
|
||||
<span className="capitalize">{t("member", { count: workspace.total_members || 0 })}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{workspace.id === activeWorkspace?.id ? (
|
||||
<span className="flex-shrink-0 p-1">
|
||||
<Check className="h-5 w-5 text-custom-sidebar-text-100" />
|
||||
</span>
|
||||
) : (
|
||||
<SubscriptionPill workspace={workspace} />
|
||||
)}
|
||||
</div>
|
||||
{workspace.id === activeWorkspace?.id && (
|
||||
<div className="mt-2 mb-1 flex gap-2">
|
||||
{workspace?.role > EUserPermissions.GUEST && (
|
||||
<Link
|
||||
href={`/${workspace.slug}/settings`}
|
||||
className="flex border border-custom-border-200 rounded-md py-1 px-2 gap-1 bg-custom-sidebar-background-100"
|
||||
>
|
||||
<Settings className="h-4 w-4 text-custom-sidebar-text-100 my-auto" />
|
||||
<span className="text-sm font-medium my-auto">{t("settings")}</span>
|
||||
</Link>
|
||||
)}
|
||||
<Link
|
||||
href={`/${workspace.slug}/settings/members`}
|
||||
className="flex border border-custom-border-200 rounded-md py-1 px-2 gap-1 bg-custom-sidebar-background-100"
|
||||
>
|
||||
<UserPlus className="h-4 w-4 text-custom-sidebar-text-100 my-auto" />
|
||||
<span className="text-sm font-medium my-auto capitalize">{t("invite")}</span>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
export default SidebarDropdownItem;
|
||||
|
|
@ -1,16 +1,14 @@
|
|||
"use client";
|
||||
|
||||
import { Fragment, Ref, useState, useMemo } from "react";
|
||||
import { Fragment, Ref, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
import { usePopper } from "react-popper";
|
||||
// icons
|
||||
import { Check, ChevronDown, LogOut, Mails, PlusSquare, Settings } from "lucide-react";
|
||||
import { ChevronDown, CirclePlus, LogOut, Mails, Settings } from "lucide-react";
|
||||
// ui
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
// types
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { IWorkspace } from "@plane/types";
|
||||
// plane ui
|
||||
|
|
@ -19,36 +17,16 @@ import { GOD_MODE_URL, cn } from "@/helpers/common.helper";
|
|||
// helpers
|
||||
import { getFileURL } from "@/helpers/file.helper";
|
||||
// hooks
|
||||
import { useAppTheme, useUser, useUserPermissions, useUserProfile, useWorkspace } from "@/hooks/store";
|
||||
// plane web constants
|
||||
import { useAppTheme, useUser, useUserProfile, useWorkspace } from "@/hooks/store";
|
||||
// plane web helpers
|
||||
import { getIsWorkspaceCreationDisabled } from "@/plane-web/helpers/instance.helper";
|
||||
// components
|
||||
import { WorkspaceLogo } from "../logo";
|
||||
import SidebarDropdownItem from "./dropdown-item";
|
||||
|
||||
export const SidebarDropdown = observer(() => {
|
||||
const { t } = useTranslation();
|
||||
const userLinks = useMemo(
|
||||
() => (workspaceSlug: string) => [
|
||||
{
|
||||
key: "workspace_invites",
|
||||
name: t("workspace_invites"),
|
||||
href: "/invitations",
|
||||
icon: Mails,
|
||||
access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST],
|
||||
},
|
||||
{
|
||||
key: "settings",
|
||||
name: t("workspace_settings.label"),
|
||||
href: `/${workspaceSlug}/settings`,
|
||||
icon: Settings,
|
||||
access: [EUserPermissions.ADMIN],
|
||||
},
|
||||
],
|
||||
[t]
|
||||
);
|
||||
// router params
|
||||
const { workspaceSlug } = useParams();
|
||||
|
||||
// store hooks
|
||||
const { sidebarCollapsed, toggleSidebar } = useAppTheme();
|
||||
const { data: currentUser } = useUser();
|
||||
|
|
@ -58,8 +36,6 @@ export const SidebarDropdown = observer(() => {
|
|||
signOut,
|
||||
} = useUser();
|
||||
const { updateUserProfile } = useUserProfile();
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
// derived values
|
||||
const isWorkspaceCreationEnabled = getIsWorkspaceCreationDisabled() === false;
|
||||
|
||||
const isUserInstanceAdmin = false;
|
||||
|
|
@ -150,57 +126,26 @@ export const SidebarDropdown = observer(() => {
|
|||
>
|
||||
<Menu.Items as={Fragment}>
|
||||
<div className="fixed top-12 left-4 z-[21] mt-1 flex w-full max-w-[19rem] origin-top-left flex-col divide-y divide-custom-border-100 rounded-md border-[0.5px] border-custom-sidebar-border-300 bg-custom-sidebar-background-100 shadow-custom-shadow-rg outline-none">
|
||||
<div className="vertical-scrollbar scrollbar-sm mb-2 flex max-h-96 flex-col items-start justify-start gap-2 overflow-y-scroll px-4">
|
||||
<h6 className="sticky top-0 z-[21] h-full w-full bg-custom-sidebar-background-100 pb-1 pt-3 text-sm font-medium text-custom-sidebar-text-400">
|
||||
<div className="overflow-x-hidden vertical-scrollbar scrollbar-sm flex max-h-96 flex-col items-start justify-start overflow-y-scroll">
|
||||
<span className="rounded-md px-4 sticky top-0 z-[21] h-full w-full bg-custom-sidebar-background-100 pb-1 pt-3 text-sm font-medium text-custom-text-400 truncate flex-shrink-0">
|
||||
{currentUser?.email}
|
||||
</h6>
|
||||
</span>
|
||||
{workspacesList ? (
|
||||
<div className="size-full flex flex-col items-start justify-start gap-1.5">
|
||||
{workspacesList.map((workspace) => (
|
||||
<Link
|
||||
<div className="size-full flex flex-col items-start justify-start">
|
||||
{(activeWorkspace
|
||||
? [
|
||||
activeWorkspace,
|
||||
...workspacesList.filter((workspace) => workspace.id !== activeWorkspace?.id),
|
||||
]
|
||||
: workspacesList
|
||||
).map((workspace) => (
|
||||
<SidebarDropdownItem
|
||||
key={workspace.id}
|
||||
href={`/${workspace.slug}`}
|
||||
onClick={() => {
|
||||
handleWorkspaceNavigation(workspace);
|
||||
handleItemClick();
|
||||
}}
|
||||
className="w-full"
|
||||
>
|
||||
<Menu.Item
|
||||
as="div"
|
||||
className="flex items-center justify-between gap-1 rounded p-1 text-sm text-custom-sidebar-text-100 hover:bg-custom-sidebar-background-80"
|
||||
>
|
||||
<div className="flex items-center justify-start gap-2.5 truncate">
|
||||
<span
|
||||
className={`relative flex h-6 w-6 flex-shrink-0 items-center justify-center p-2 text-xs uppercase ${
|
||||
!workspace?.logo_url && "rounded bg-custom-primary-500 text-white"
|
||||
}`}
|
||||
>
|
||||
{workspace?.logo_url && workspace.logo_url !== "" ? (
|
||||
<img
|
||||
src={getFileURL(workspace.logo_url)}
|
||||
className="absolute left-0 top-0 h-full w-full rounded object-cover"
|
||||
alt={t("workspace_logo")}
|
||||
/>
|
||||
) : (
|
||||
(workspace?.name?.[0] ?? "...")
|
||||
)}
|
||||
</span>
|
||||
<h5
|
||||
className={`truncate text-sm font-medium ${
|
||||
workspaceSlug === workspace.slug ? "" : "text-custom-text-200"
|
||||
}`}
|
||||
>
|
||||
{workspace.name}
|
||||
</h5>
|
||||
</div>
|
||||
{workspace.id === activeWorkspace?.id && (
|
||||
<span className="flex-shrink-0 p-1">
|
||||
<Check className="h-5 w-5 text-custom-sidebar-text-100" />
|
||||
</span>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</Link>
|
||||
workspace={workspace}
|
||||
activeWorkspace={activeWorkspace}
|
||||
handleItemClick={handleItemClick}
|
||||
handleWorkspaceNavigation={handleWorkspaceNavigation}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
|
|
@ -219,43 +164,33 @@ export const SidebarDropdown = observer(() => {
|
|||
as="div"
|
||||
className="flex items-center gap-2 rounded px-2 py-1 text-sm font-medium text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80"
|
||||
>
|
||||
<PlusSquare strokeWidth={1.75} className="h-4 w-4 flex-shrink-0" />
|
||||
<CirclePlus className="size-4 flex-shrink-0" />
|
||||
{t("create_workspace")}
|
||||
</Menu.Item>
|
||||
</Link>
|
||||
)}
|
||||
{userLinks(workspaceSlug?.toString() ?? "").map(
|
||||
(link, index) =>
|
||||
allowPermissions(link.access, EUserPermissionsLevel.WORKSPACE) && (
|
||||
<Link
|
||||
key={link.key}
|
||||
href={link.href}
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
if (index > 0) handleItemClick();
|
||||
}}
|
||||
>
|
||||
<Menu.Item
|
||||
as="div"
|
||||
className="flex items-center gap-2 rounded px-2 py-1 text-sm font-medium text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80"
|
||||
>
|
||||
<link.icon className="h-4 w-4 flex-shrink-0" />
|
||||
{link.name}
|
||||
</Menu.Item>
|
||||
</Link>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<div className="w-full px-4 py-2">
|
||||
<Menu.Item
|
||||
as="button"
|
||||
type="button"
|
||||
className="flex w-full items-center gap-2 rounded px-2 py-1 text-sm font-medium text-red-600 hover:bg-custom-sidebar-background-80"
|
||||
onClick={handleSignOut}
|
||||
>
|
||||
<LogOut className="size-4 flex-shrink-0" />
|
||||
{t("sign_out")}
|
||||
</Menu.Item>
|
||||
|
||||
<Link href="/invitations" className="w-full" onClick={handleItemClick}>
|
||||
<Menu.Item
|
||||
as="div"
|
||||
className="flex items-center gap-2 rounded px-2 py-1 text-sm font-medium text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80"
|
||||
>
|
||||
<Mails className="h-4 w-4 flex-shrink-0" />
|
||||
{t("workspace_invites")}
|
||||
</Menu.Item>
|
||||
</Link>
|
||||
|
||||
<div className="w-full">
|
||||
<Menu.Item
|
||||
as="button"
|
||||
type="button"
|
||||
className="flex w-full items-center gap-2 rounded px-2 py-1 text-sm font-medium text-red-600 hover:bg-custom-sidebar-background-80"
|
||||
onClick={handleSignOut}
|
||||
>
|
||||
<LogOut className="size-4 flex-shrink-0" />
|
||||
{t("sign_out")}
|
||||
</Menu.Item>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
|
|
|
|||
1
web/ee/components/common/subscription-pill.tsx
Normal file
1
web/ee/components/common/subscription-pill.tsx
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from "ce/components/common/subscription-pill";
|
||||
Loading…
Add table
Add a link
Reference in a new issue