Skip to content

Commit 20ba91b

Browse files
gakshitasangeethailangoanmolsinghbhatia
authored
[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>
1 parent 456c7f5 commit 20ba91b

File tree

7 files changed

+172
-112
lines changed

7 files changed

+172
-112
lines changed

apiserver/plane/app/serializers/workspace.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,10 @@
3232

3333

3434
class WorkSpaceSerializer(DynamicBaseSerializer):
35-
owner = UserLiteSerializer(read_only=True)
3635
total_members = serializers.IntegerField(read_only=True)
3736
total_issues = serializers.IntegerField(read_only=True)
3837
logo_url = serializers.CharField(read_only=True)
38+
role = serializers.IntegerField(read_only=True)
3939

4040
def validate_slug(self, value):
4141
# Check if the slug is restricted

apiserver/plane/app/views/workspace/base.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77
from dateutil.relativedelta import relativedelta
88
from django.db import IntegrityError
99
from django.db.models import Count, F, Func, OuterRef, Prefetch, Q
10+
1011
from django.db.models.fields import DateField
1112
from django.db.models.functions import Cast, ExtractDay, ExtractWeek
1213

14+
1315
# Django imports
1416
from django.http import HttpResponse
1517
from django.utils import timezone
@@ -173,6 +175,11 @@ def get(self, request):
173175
.values("count")
174176
)
175177

178+
role = (
179+
WorkspaceMember.objects.filter(workspace=OuterRef("id"), member=request.user, is_active=True)
180+
.values("role")
181+
)
182+
176183
workspace = (
177184
Workspace.objects.prefetch_related(
178185
Prefetch(
@@ -184,17 +191,19 @@ def get(self, request):
184191
)
185192
.select_related("owner")
186193
.annotate(total_members=member_count)
187-
.annotate(total_issues=issue_count)
194+
.annotate(total_issues=issue_count, role=role)
188195
.filter(
189196
workspace_member__member=request.user, workspace_member__is_active=True
190197
)
191198
.distinct()
192199
)
200+
193201
workspaces = WorkSpaceSerializer(
194202
self.filter_queryset(workspace),
195203
fields=fields if fields else None,
196204
many=True,
197205
).data
206+
198207
return Response(workspaces, status=status.HTTP_200_OK)
199208

200209

packages/types/src/workspace.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ export interface IWorkspace {
2323
organization_size: string;
2424
total_issues: number;
2525
total_projects?: number;
26+
current_plan?: string;
27+
role: number;
2628
}
2729

2830
export interface IWorkspaceLite {
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { IWorkspace } from "@plane/types";
2+
3+
type TProps = {
4+
workspace: IWorkspace;
5+
};
6+
7+
export const SubscriptionPill = (props: TProps) => <></>;
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import Link from "next/link";
2+
import { useParams } from "next/navigation";
3+
import { Check, Settings, UserPlus } from "lucide-react";
4+
import { Menu } from "@headlessui/react";
5+
import { EUserPermissions } from "@plane/constants";
6+
import { useTranslation } from "@plane/i18n";
7+
import { IWorkspace } from "@plane/types";
8+
import { cn, getFileURL } from "@plane/utils";
9+
import { getUserRole } from "@/helpers/user.helper";
10+
import { SubscriptionPill } from "@/plane-web/components/common/subscription-pill";
11+
12+
type TProps = {
13+
workspace: IWorkspace;
14+
activeWorkspace: IWorkspace | null;
15+
handleItemClick: () => void;
16+
handleWorkspaceNavigation: (workspace: IWorkspace) => void;
17+
};
18+
const SidebarDropdownItem = (props: TProps) => {
19+
const { workspace, activeWorkspace, handleItemClick, handleWorkspaceNavigation } = props;
20+
21+
// router params
22+
const { workspaceSlug } = useParams();
23+
const { t } = useTranslation();
24+
25+
return (
26+
<Link
27+
key={workspace.id}
28+
href={`/${workspace.slug}`}
29+
onClick={() => {
30+
handleWorkspaceNavigation(workspace);
31+
handleItemClick();
32+
}}
33+
className="w-full"
34+
id={workspace.id}
35+
>
36+
<Menu.Item
37+
as="div"
38+
className={cn("px-4 py-2", {
39+
"bg-custom-sidebar-background-90": workspace.id === activeWorkspace?.id,
40+
"hover:bg-custom-sidebar-background-90": workspace.id !== activeWorkspace?.id,
41+
})}
42+
>
43+
<div className="flex items-center justify-between gap-1 rounded p-1 text-sm text-custom-sidebar-text-100 ">
44+
<div className="flex items-center justify-start gap-2.5 w-[80%] relative">
45+
<span
46+
className={`relative flex h-8 w-8 flex-shrink-0 items-center justify-center p-2 text-sm uppercase font-semibold ${
47+
!workspace?.logo_url && "rounded-lg bg-custom-primary-500 text-white"
48+
}`}
49+
>
50+
{workspace?.logo_url && workspace.logo_url !== "" ? (
51+
<img
52+
src={getFileURL(workspace.logo_url)}
53+
className="absolute left-0 top-0 h-full w-full rounded-lg object-cover"
54+
alt={t("workspace_logo")}
55+
/>
56+
) : (
57+
(workspace?.name?.[0] ?? "...")
58+
)}
59+
</span>
60+
<div className="w-[inherit]">
61+
<div
62+
className={`truncate text-ellipsis text-sm font-medium ${workspaceSlug === workspace.slug ? "" : "text-custom-text-200"}`}
63+
>
64+
{workspace.name}
65+
</div>
66+
<div className="text-sm text-custom-text-300 flex gap-2 capitalize w-fit">
67+
<span>{getUserRole(workspace.role)?.toLowerCase() || "guest"}</span>
68+
<div className="w-1 h-1 bg-custom-text-300/50 rounded-full m-auto" />
69+
<span className="capitalize">{t("member", { count: workspace.total_members || 0 })}</span>
70+
</div>
71+
</div>
72+
</div>
73+
{workspace.id === activeWorkspace?.id ? (
74+
<span className="flex-shrink-0 p-1">
75+
<Check className="h-5 w-5 text-custom-sidebar-text-100" />
76+
</span>
77+
) : (
78+
<SubscriptionPill workspace={workspace} />
79+
)}
80+
</div>
81+
{workspace.id === activeWorkspace?.id && (
82+
<div className="mt-2 mb-1 flex gap-2">
83+
{workspace?.role > EUserPermissions.GUEST && (
84+
<Link
85+
href={`/${workspace.slug}/settings`}
86+
className="flex border border-custom-border-200 rounded-md py-1 px-2 gap-1 bg-custom-sidebar-background-100"
87+
>
88+
<Settings className="h-4 w-4 text-custom-sidebar-text-100 my-auto" />
89+
<span className="text-sm font-medium my-auto">{t("settings")}</span>
90+
</Link>
91+
)}
92+
<Link
93+
href={`/${workspace.slug}/settings/members`}
94+
className="flex border border-custom-border-200 rounded-md py-1 px-2 gap-1 bg-custom-sidebar-background-100"
95+
>
96+
<UserPlus className="h-4 w-4 text-custom-sidebar-text-100 my-auto" />
97+
<span className="text-sm font-medium my-auto capitalize">{t("invite")}</span>
98+
</Link>
99+
</div>
100+
)}
101+
</Menu.Item>
102+
</Link>
103+
);
104+
};
105+
106+
export default SidebarDropdownItem;

web/core/components/workspace/sidebar/dropdown.tsx

Lines changed: 45 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
11
"use client";
22

3-
import { Fragment, Ref, useState, useMemo } from "react";
3+
import { Fragment, Ref, useState } from "react";
44
import { observer } from "mobx-react";
55
import Link from "next/link";
6-
import { useParams } from "next/navigation";
76
import { usePopper } from "react-popper";
87
// icons
9-
import { Check, ChevronDown, LogOut, Mails, PlusSquare, Settings } from "lucide-react";
8+
import { ChevronDown, CirclePlus, LogOut, Mails, Settings } from "lucide-react";
109
// ui
1110
import { Menu, Transition } from "@headlessui/react";
1211
// types
13-
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
1412
import { useTranslation } from "@plane/i18n";
1513
import { IWorkspace } from "@plane/types";
1614
// plane ui
@@ -19,36 +17,16 @@ import { GOD_MODE_URL, cn } from "@/helpers/common.helper";
1917
// helpers
2018
import { getFileURL } from "@/helpers/file.helper";
2119
// hooks
22-
import { useAppTheme, useUser, useUserPermissions, useUserProfile, useWorkspace } from "@/hooks/store";
23-
// plane web constants
20+
import { useAppTheme, useUser, useUserProfile, useWorkspace } from "@/hooks/store";
2421
// plane web helpers
2522
import { getIsWorkspaceCreationDisabled } from "@/plane-web/helpers/instance.helper";
2623
// components
2724
import { WorkspaceLogo } from "../logo";
25+
import SidebarDropdownItem from "./dropdown-item";
2826

2927
export const SidebarDropdown = observer(() => {
3028
const { t } = useTranslation();
31-
const userLinks = useMemo(
32-
() => (workspaceSlug: string) => [
33-
{
34-
key: "workspace_invites",
35-
name: t("workspace_invites"),
36-
href: "/invitations",
37-
icon: Mails,
38-
access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST],
39-
},
40-
{
41-
key: "settings",
42-
name: t("workspace_settings.label"),
43-
href: `/${workspaceSlug}/settings`,
44-
icon: Settings,
45-
access: [EUserPermissions.ADMIN],
46-
},
47-
],
48-
[t]
49-
);
50-
// router params
51-
const { workspaceSlug } = useParams();
29+
5230
// store hooks
5331
const { sidebarCollapsed, toggleSidebar } = useAppTheme();
5432
const { data: currentUser } = useUser();
@@ -58,8 +36,6 @@ export const SidebarDropdown = observer(() => {
5836
signOut,
5937
} = useUser();
6038
const { updateUserProfile } = useUserProfile();
61-
const { allowPermissions } = useUserPermissions();
62-
// derived values
6339
const isWorkspaceCreationEnabled = getIsWorkspaceCreationDisabled() === false;
6440

6541
const isUserInstanceAdmin = false;
@@ -150,57 +126,26 @@ export const SidebarDropdown = observer(() => {
150126
>
151127
<Menu.Items as={Fragment}>
152128
<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">
153-
<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">
154-
<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">
129+
<div className="overflow-x-hidden vertical-scrollbar scrollbar-sm flex max-h-96 flex-col items-start justify-start overflow-y-scroll">
130+
<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">
155131
{currentUser?.email}
156-
</h6>
132+
</span>
157133
{workspacesList ? (
158-
<div className="size-full flex flex-col items-start justify-start gap-1.5">
159-
{workspacesList.map((workspace) => (
160-
<Link
134+
<div className="size-full flex flex-col items-start justify-start">
135+
{(activeWorkspace
136+
? [
137+
activeWorkspace,
138+
...workspacesList.filter((workspace) => workspace.id !== activeWorkspace?.id),
139+
]
140+
: workspacesList
141+
).map((workspace) => (
142+
<SidebarDropdownItem
161143
key={workspace.id}
162-
href={`/${workspace.slug}`}
163-
onClick={() => {
164-
handleWorkspaceNavigation(workspace);
165-
handleItemClick();
166-
}}
167-
className="w-full"
168-
>
169-
<Menu.Item
170-
as="div"
171-
className="flex items-center justify-between gap-1 rounded p-1 text-sm text-custom-sidebar-text-100 hover:bg-custom-sidebar-background-80"
172-
>
173-
<div className="flex items-center justify-start gap-2.5 truncate">
174-
<span
175-
className={`relative flex h-6 w-6 flex-shrink-0 items-center justify-center p-2 text-xs uppercase ${
176-
!workspace?.logo_url && "rounded bg-custom-primary-500 text-white"
177-
}`}
178-
>
179-
{workspace?.logo_url && workspace.logo_url !== "" ? (
180-
<img
181-
src={getFileURL(workspace.logo_url)}
182-
className="absolute left-0 top-0 h-full w-full rounded object-cover"
183-
alt={t("workspace_logo")}
184-
/>
185-
) : (
186-
(workspace?.name?.[0] ?? "...")
187-
)}
188-
</span>
189-
<h5
190-
className={`truncate text-sm font-medium ${
191-
workspaceSlug === workspace.slug ? "" : "text-custom-text-200"
192-
}`}
193-
>
194-
{workspace.name}
195-
</h5>
196-
</div>
197-
{workspace.id === activeWorkspace?.id && (
198-
<span className="flex-shrink-0 p-1">
199-
<Check className="h-5 w-5 text-custom-sidebar-text-100" />
200-
</span>
201-
)}
202-
</Menu.Item>
203-
</Link>
144+
workspace={workspace}
145+
activeWorkspace={activeWorkspace}
146+
handleItemClick={handleItemClick}
147+
handleWorkspaceNavigation={handleWorkspaceNavigation}
148+
/>
204149
))}
205150
</div>
206151
) : (
@@ -219,43 +164,33 @@ export const SidebarDropdown = observer(() => {
219164
as="div"
220165
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"
221166
>
222-
<PlusSquare strokeWidth={1.75} className="h-4 w-4 flex-shrink-0" />
167+
<CirclePlus className="size-4 flex-shrink-0" />
223168
{t("create_workspace")}
224169
</Menu.Item>
225170
</Link>
226171
)}
227-
{userLinks(workspaceSlug?.toString() ?? "").map(
228-
(link, index) =>
229-
allowPermissions(link.access, EUserPermissionsLevel.WORKSPACE) && (
230-
<Link
231-
key={link.key}
232-
href={link.href}
233-
className="w-full"
234-
onClick={() => {
235-
if (index > 0) handleItemClick();
236-
}}
237-
>
238-
<Menu.Item
239-
as="div"
240-
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"
241-
>
242-
<link.icon className="h-4 w-4 flex-shrink-0" />
243-
{link.name}
244-
</Menu.Item>
245-
</Link>
246-
)
247-
)}
248-
</div>
249-
<div className="w-full px-4 py-2">
250-
<Menu.Item
251-
as="button"
252-
type="button"
253-
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"
254-
onClick={handleSignOut}
255-
>
256-
<LogOut className="size-4 flex-shrink-0" />
257-
{t("sign_out")}
258-
</Menu.Item>
172+
173+
<Link href="/invitations" className="w-full" onClick={handleItemClick}>
174+
<Menu.Item
175+
as="div"
176+
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"
177+
>
178+
<Mails className="h-4 w-4 flex-shrink-0" />
179+
{t("workspace_invites")}
180+
</Menu.Item>
181+
</Link>
182+
183+
<div className="w-full">
184+
<Menu.Item
185+
as="button"
186+
type="button"
187+
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"
188+
onClick={handleSignOut}
189+
>
190+
<LogOut className="size-4 flex-shrink-0" />
191+
{t("sign_out")}
192+
</Menu.Item>
193+
</div>
259194
</div>
260195
</div>
261196
</Menu.Items>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "ce/components/common/subscription-pill";

0 commit comments

Comments
 (0)