diff --git a/apiserver/plane/app/serializers/workspace.py b/apiserver/plane/app/serializers/workspace.py index 68e12608517..3467fff9960 100644 --- a/apiserver/plane/app/serializers/workspace.py +++ b/apiserver/plane/app/serializers/workspace.py @@ -32,10 +32,10 @@ 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 diff --git a/apiserver/plane/app/views/workspace/base.py b/apiserver/plane/app/views/workspace/base.py index 058f7702abd..4d81eb1607d 100644 --- a/apiserver/plane/app/views/workspace/base.py +++ b/apiserver/plane/app/views/workspace/base.py @@ -7,9 +7,11 @@ 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 @@ def get(self, request): .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 @@ def get(self, request): ) .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) diff --git a/packages/types/src/workspace.d.ts b/packages/types/src/workspace.d.ts index 021ea173e35..b68cddd7d01 100644 --- a/packages/types/src/workspace.d.ts +++ b/packages/types/src/workspace.d.ts @@ -23,6 +23,8 @@ export interface IWorkspace { organization_size: string; total_issues: number; total_projects?: number; + current_plan?: string; + role: number; } export interface IWorkspaceLite { diff --git a/web/ce/components/common/subscription-pill.tsx b/web/ce/components/common/subscription-pill.tsx new file mode 100644 index 00000000000..c557ce204b5 --- /dev/null +++ b/web/ce/components/common/subscription-pill.tsx @@ -0,0 +1,7 @@ +import { IWorkspace } from "@plane/types"; + +type TProps = { + workspace: IWorkspace; +}; + +export const SubscriptionPill = (props: TProps) => <>; diff --git a/web/core/components/workspace/sidebar/dropdown-item.tsx b/web/core/components/workspace/sidebar/dropdown-item.tsx new file mode 100644 index 00000000000..b2519783135 --- /dev/null +++ b/web/core/components/workspace/sidebar/dropdown-item.tsx @@ -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 ( + { + handleWorkspaceNavigation(workspace); + handleItemClick(); + }} + className="w-full" + id={workspace.id} + > + +
+
+ + {workspace?.logo_url && workspace.logo_url !== "" ? ( + {t("workspace_logo")} + ) : ( + (workspace?.name?.[0] ?? "...") + )} + +
+
+ {workspace.name} +
+
+ {getUserRole(workspace.role)?.toLowerCase() || "guest"} +
+ {t("member", { count: workspace.total_members || 0 })} +
+
+
+ {workspace.id === activeWorkspace?.id ? ( + + + + ) : ( + + )} +
+ {workspace.id === activeWorkspace?.id && ( +
+ {workspace?.role > EUserPermissions.GUEST && ( + + + {t("settings")} + + )} + + + {t("invite")} + +
+ )} + + + ); +}; + +export default SidebarDropdownItem; diff --git a/web/core/components/workspace/sidebar/dropdown.tsx b/web/core/components/workspace/sidebar/dropdown.tsx index 00993352820..8d131594840 100644 --- a/web/core/components/workspace/sidebar/dropdown.tsx +++ b/web/core/components/workspace/sidebar/dropdown.tsx @@ -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(() => { >
-
-
+
+ {currentUser?.email} -
+ {workspacesList ? ( -
- {workspacesList.map((workspace) => ( - + {(activeWorkspace + ? [ + activeWorkspace, + ...workspacesList.filter((workspace) => workspace.id !== activeWorkspace?.id), + ] + : workspacesList + ).map((workspace) => ( + { - handleWorkspaceNavigation(workspace); - handleItemClick(); - }} - className="w-full" - > - -
- - {workspace?.logo_url && workspace.logo_url !== "" ? ( - {t("workspace_logo")} - ) : ( - (workspace?.name?.[0] ?? "...") - )} - -
- {workspace.name} -
-
- {workspace.id === activeWorkspace?.id && ( - - - - )} -
- + workspace={workspace} + activeWorkspace={activeWorkspace} + handleItemClick={handleItemClick} + handleWorkspaceNavigation={handleWorkspaceNavigation} + /> ))}
) : ( @@ -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" > - + {t("create_workspace")} )} - {userLinks(workspaceSlug?.toString() ?? "").map( - (link, index) => - allowPermissions(link.access, EUserPermissionsLevel.WORKSPACE) && ( - { - if (index > 0) handleItemClick(); - }} - > - - - {link.name} - - - ) - )} -
-
- - - {t("sign_out")} - + + + + + {t("workspace_invites")} + + + +
+ + + {t("sign_out")} + +
diff --git a/web/ee/components/common/subscription-pill.tsx b/web/ee/components/common/subscription-pill.tsx new file mode 100644 index 00000000000..38bcdebd5ad --- /dev/null +++ b/web/ee/components/common/subscription-pill.tsx @@ -0,0 +1 @@ +export * from "ce/components/common/subscription-pill";