[WEB-5758] feat: tips section (#5505)
* feat: endpoint to update onboarding related fields * fix: endpoint and serializer name * chore: explored features types, store and service updated * chore sidebar wrapper component updated * feat: try section implemeantion * fix: format error * chore: tips asset added * chore: tips types updated * chore: tips services added * feat: tips section implementation * chore: code refactoring * chore: code refactoring * fix: validation * fix: type build error * fix: format build error --------- Co-authored-by: sangeethailango <[email protected]>
This commit is contained in:
co-authored by
sangeethailango
parent
06d05ed34f
commit
91e44834cb
@@ -35,6 +35,7 @@ from .workspace import (
|
||||
WorkspaceHomePreferenceSerializer,
|
||||
StickySerializer,
|
||||
WorkspaceUserMeSerializer,
|
||||
WorkspaceMemberUserOnboardingSerializer,
|
||||
)
|
||||
from .project import (
|
||||
ProjectSerializer,
|
||||
|
||||
@@ -131,6 +131,13 @@ class WorkspaceMemberMeSerializer(BaseSerializer):
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class WorkspaceMemberUserOnboardingSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = WorkspaceMember
|
||||
fields = ["workspace", "member", "getting_started_checklist", "tips", "explored_features"]
|
||||
read_only_fields = ["workspace", "member"]
|
||||
|
||||
|
||||
class WorkspaceMemberAdminSerializer(DynamicBaseSerializer):
|
||||
member = UserAdminLiteSerializer(read_only=True)
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ from plane.app.views import (
|
||||
WorkspaceHomePreferenceViewSet,
|
||||
WorkspaceStickyViewSet,
|
||||
WorkspaceUserPreferenceViewSet,
|
||||
WorkspaceMemberUserOnboardingEndpoint,
|
||||
)
|
||||
|
||||
|
||||
@@ -264,4 +265,10 @@ urlpatterns = [
|
||||
WorkspaceUserPreferenceViewSet.as_view(),
|
||||
name="workspace-user-preference",
|
||||
),
|
||||
# Onboading product tour
|
||||
path(
|
||||
"workspaces/<str:slug>/workspace-member/me/onboarding/",
|
||||
WorkspaceMemberUserOnboardingEndpoint.as_view(),
|
||||
name="workspace-member-onboading",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -66,6 +66,7 @@ from .workspace.member import (
|
||||
WorkspaceMemberUserEndpoint,
|
||||
WorkspaceProjectMemberEndpoint,
|
||||
WorkspaceMemberUserViewsEndpoint,
|
||||
WorkspaceMemberUserOnboardingEndpoint,
|
||||
)
|
||||
from .workspace.invite import (
|
||||
WorkspaceInvitationsViewset,
|
||||
|
||||
@@ -30,6 +30,7 @@ from plane.app.serializers import (
|
||||
WorkspaceMemberAdminSerializer,
|
||||
WorkspaceMemberMeSerializer,
|
||||
WorkSpaceMemberSerializer,
|
||||
WorkspaceMemberUserOnboardingSerializer,
|
||||
)
|
||||
from plane.app.views.base import BaseAPIView
|
||||
from plane.utils.cache import invalidate_cache
|
||||
@@ -344,6 +345,34 @@ class WorkspaceMemberUserEndpoint(BaseAPIView):
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
# For WorkspaceMember fields:
|
||||
# - getting_started_checklist
|
||||
# - tips
|
||||
# - explored_features
|
||||
class WorkspaceMemberUserOnboardingEndpoint(BaseAPIView):
|
||||
def patch(self, request, slug):
|
||||
try:
|
||||
workspace_member = WorkspaceMember.objects.get(workspace__slug=slug, member_id=request.user.id)
|
||||
|
||||
except WorkspaceMember.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "You are not a member of this workspace"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
serializer = WorkspaceMemberUserOnboardingSerializer(
|
||||
workspace_member,
|
||||
data=request.data,
|
||||
partial=True,
|
||||
)
|
||||
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
return Response(serializer.errors, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class WorkspaceProjectMemberEndpoint(BaseAPIView):
|
||||
serializer_class = ProjectMemberRoleSerializer
|
||||
model = ProjectMember
|
||||
|
||||
@@ -11,44 +11,39 @@
|
||||
* NOTICE: Proprietary and confidential. Unauthorized use or distribution is prohibited.
|
||||
*/
|
||||
|
||||
import type { FC } from "react";
|
||||
import { isEmpty } from "lodash-es";
|
||||
import { useMemo } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// plane helpers
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
// components
|
||||
import { SidebarWrapper } from "@/components/sidebar/sidebar-wrapper";
|
||||
import { SidebarFavoritesMenu } from "@/components/workspace/sidebar/favorites/favorites-menu";
|
||||
import { SidebarProjectsList } from "@/components/workspace/sidebar/projects-list";
|
||||
import { SidebarQuickActions } from "@/components/workspace/sidebar/quick-actions";
|
||||
import { SidebarMenuItems } from "@/components/workspace/sidebar/sidebar-menu-items";
|
||||
// hooks
|
||||
import { useFavorite } from "@/hooks/store/use-favorite";
|
||||
import { useUserPermissions } from "@/hooks/store/user";
|
||||
// plane web components
|
||||
import { SidebarTeamsList } from "@/plane-web/components/workspace/sidebar/teamspaces/root";
|
||||
|
||||
export const AppSidebar = observer(function AppSidebar() {
|
||||
// store hooks
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
const { groupedFavorites } = useFavorite();
|
||||
|
||||
// derived values
|
||||
const canPerformWorkspaceMemberActions = allowPermissions(
|
||||
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
|
||||
EUserPermissionsLevel.WORKSPACE
|
||||
const canPerformWorkspaceMemberActions = useMemo(
|
||||
() => allowPermissions([EUserPermissions.ADMIN, EUserPermissions.MEMBER], EUserPermissionsLevel.WORKSPACE),
|
||||
[allowPermissions]
|
||||
);
|
||||
|
||||
const isFavoriteEmpty = isEmpty(groupedFavorites);
|
||||
const hasFavorites = useMemo(() => {
|
||||
if (!groupedFavorites) return false;
|
||||
return Object.keys(groupedFavorites).length > 0;
|
||||
}, [groupedFavorites]);
|
||||
|
||||
const showFavorites = canPerformWorkspaceMemberActions && hasFavorites;
|
||||
|
||||
return (
|
||||
<SidebarWrapper title="Projects" quickActions={<SidebarQuickActions />}>
|
||||
<SidebarMenuItems />
|
||||
{/* Favorites Menu */}
|
||||
{canPerformWorkspaceMemberActions && !isFavoriteEmpty && <SidebarFavoritesMenu />}
|
||||
{/* Teams List */}
|
||||
{showFavorites && <SidebarFavoritesMenu />}
|
||||
<SidebarTeamsList />
|
||||
{/* Projects List */}
|
||||
<SidebarProjectsList />
|
||||
</SidebarWrapper>
|
||||
);
|
||||
|
||||
@@ -11,9 +11,10 @@
|
||||
* NOTICE: Proprietary and confidential. Unauthorized use or distribution is prohibited.
|
||||
*/
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// plane helpers
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { useOutsideClickDetector } from "@plane/hooks";
|
||||
import { PreferencesIcon } from "@plane/propel/icons";
|
||||
import { ScrollArea } from "@plane/propel/scrollarea";
|
||||
@@ -21,11 +22,14 @@ import { ScrollArea } from "@plane/propel/scrollarea";
|
||||
import { CustomizeNavigationDialog } from "@/components/navigation/customize-navigation-dialog";
|
||||
// hooks
|
||||
import { useAppTheme } from "@/hooks/store/use-app-theme";
|
||||
import { useUserPermissions } from "@/hooks/store/user/user-permissions";
|
||||
import useSize from "@/hooks/use-window-size";
|
||||
// plane web components
|
||||
import { WorkspaceEditionBadge } from "@/plane-web/components/workspace/edition-badge";
|
||||
import { AppSidebarToggleButton } from "./sidebar-toggle-button";
|
||||
import { IconButton } from "@plane/propel/icon-button";
|
||||
import { SidebarTipSection } from "../workspace/sidebar/sidebar-tip-section";
|
||||
import { SidebarTrySection } from "../workspace/sidebar/sidebar-try-section";
|
||||
|
||||
type TSidebarWrapperProps = {
|
||||
title: string;
|
||||
@@ -40,9 +44,16 @@ export const SidebarWrapper = observer(function SidebarWrapper(props: TSidebarWr
|
||||
// store hooks
|
||||
const { toggleSidebar, sidebarCollapsed } = useAppTheme();
|
||||
const windowSize = useSize();
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
|
||||
// refs
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const isWorkspaceAdmin = useMemo(
|
||||
() => allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE),
|
||||
[allowPermissions]
|
||||
);
|
||||
|
||||
useOutsideClickDetector(ref, () => {
|
||||
if (sidebarCollapsed === false && window.innerWidth < 768) {
|
||||
toggleSidebar();
|
||||
@@ -57,7 +68,7 @@ export const SidebarWrapper = observer(function SidebarWrapper(props: TSidebarWr
|
||||
return (
|
||||
<>
|
||||
<CustomizeNavigationDialog isOpen={isCustomizeNavDialogOpen} onClose={() => setIsCustomizeNavDialogOpen(false)} />
|
||||
<div ref={ref} className="animate-fade-in flex flex-col h-full w-full">
|
||||
<div ref={ref} className="relative animate-fade-in flex flex-col h-full w-full">
|
||||
<div className="flex flex-col gap-3 px-3">
|
||||
{/* Workspace switcher and settings */}
|
||||
|
||||
@@ -90,13 +101,16 @@ export const SidebarWrapper = observer(function SidebarWrapper(props: TSidebarWr
|
||||
{children}
|
||||
</ScrollArea>
|
||||
{/* Help Section */}
|
||||
<div className="flex items-center justify-between p-3 border-t border-subtle bg-surface-1 h-12">
|
||||
<WorkspaceEditionBadge />
|
||||
{/* TODO: To be checked if we need this */}
|
||||
{/* <div className="flex items-center gap-2">
|
||||
{!shouldRenderAppRail && <HelpMenu />}
|
||||
{!isAppRailEnabled && <AppSidebarToggleButton />}
|
||||
</div> */}
|
||||
<div className="flex flex-col">
|
||||
{isWorkspaceAdmin && (
|
||||
<div className="flex flex-col gap-2 p-3">
|
||||
<SidebarTrySection />
|
||||
<SidebarTipSection />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center justify-between p-3 border-t border-subtle bg-surface-1 h-12">
|
||||
<WorkspaceEditionBadge />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2023-present Plane Software, Inc.
|
||||
* SPDX-License-Identifier: LicenseRef-Plane-Commercial
|
||||
*
|
||||
* Licensed under the Plane Commercial License (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* https://plane.so/legals/eula
|
||||
*
|
||||
* DO NOT remove or modify this notice.
|
||||
* NOTICE: Proprietary and confidential. Unauthorized use or distribution is prohibited.
|
||||
*/
|
||||
|
||||
import { useMemo, useCallback } from "react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { useTheme } from "next-themes";
|
||||
import { observer } from "mobx-react";
|
||||
import type { TTips } from "@plane/types";
|
||||
import { IconButton } from "@plane/propel/icon-button";
|
||||
import { CloseIcon } from "@plane/propel/icons";
|
||||
import { useUserPermissions } from "@/hooks/store/user";
|
||||
|
||||
type TTipItem = {
|
||||
id: TTips;
|
||||
title: string;
|
||||
description: string;
|
||||
imageLight: string;
|
||||
imageDark: string;
|
||||
};
|
||||
|
||||
const TIPS_CONFIG: TTipItem[] = [
|
||||
{
|
||||
id: "mobile_app_download",
|
||||
title: "Download Plane for mobile",
|
||||
description: "Manage your work on the go. Download Plane on Play Store and App Store.",
|
||||
imageLight: "/tips/download-app/download-app-light.webp",
|
||||
imageDark: "/tips/download-app/download-app-dark.webp",
|
||||
},
|
||||
// Add more tips here in the future
|
||||
];
|
||||
|
||||
export const SidebarTipSection = observer(() => {
|
||||
const { workspaceSlug } = useParams();
|
||||
const { resolvedTheme } = useTheme();
|
||||
const { workspaceInfoBySlug, updateTips } = useUserPermissions();
|
||||
|
||||
const workspaceSlugStr = workspaceSlug?.toString() ?? "";
|
||||
const currentWorkspaceInfo = workspaceInfoBySlug(workspaceSlugStr);
|
||||
|
||||
// Find the first tip that hasn't been dismissed
|
||||
const activeTip = useMemo(() => {
|
||||
if (!currentWorkspaceInfo?.tips) return TIPS_CONFIG[0];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
return TIPS_CONFIG.find((tip) => currentWorkspaceInfo.tips[tip.id] !== true) ?? null;
|
||||
}, [currentWorkspaceInfo?.tips]);
|
||||
|
||||
const backgroundImage = useMemo(
|
||||
() => (activeTip ? (resolvedTheme === "dark" ? activeTip.imageDark : activeTip.imageLight) : ""),
|
||||
[resolvedTheme, activeTip]
|
||||
);
|
||||
|
||||
const handleDismissTip = useCallback(() => {
|
||||
if (!workspaceSlugStr || !activeTip) return;
|
||||
void updateTips(workspaceSlugStr, { [activeTip.id]: true });
|
||||
}, [workspaceSlugStr, activeTip, updateTips]);
|
||||
|
||||
const backgroundStyle = useMemo(
|
||||
() => ({
|
||||
backgroundImage: `url(${backgroundImage})`,
|
||||
backgroundSize: "cover" as const,
|
||||
backgroundPosition: "center" as const,
|
||||
backgroundRepeat: "no-repeat" as const,
|
||||
}),
|
||||
[backgroundImage]
|
||||
);
|
||||
|
||||
// Don't show if no active tip
|
||||
if (!activeTip) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="absolute bottom-0 left-0 right-0 z-100 flex justify-center items-center p-2.5 w-full">
|
||||
<div className="w-full z-100 flex rounded-lg flex-col border border-subtle-1 bg-surface-2 shadow-raised-100">
|
||||
<span className="relative min-h-36 w-full bg-layer-1 rounded-t-lg" style={backgroundStyle}>
|
||||
<IconButton
|
||||
icon={CloseIcon}
|
||||
className="absolute top-2 right-2"
|
||||
onClick={handleDismissTip}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
aria-label="Dismiss tip"
|
||||
/>
|
||||
</span>
|
||||
<div className="flex flex-col gap-1 p-3">
|
||||
<span className="text-body-xs-semibold text-primary">{activeTip.title}</span>
|
||||
<span className="text-body-xs-regular text-tertiary">{activeTip.description}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2023-present Plane Software, Inc.
|
||||
* SPDX-License-Identifier: LicenseRef-Plane-Commercial
|
||||
*
|
||||
* Licensed under the Plane Commercial License (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* https://plane.so/legals/eula
|
||||
*
|
||||
* DO NOT remove or modify this notice.
|
||||
* NOTICE: Proprietary and confidential. Unauthorized use or distribution is prohibited.
|
||||
*/
|
||||
|
||||
import { useState, useMemo, useCallback } from "react";
|
||||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
import { Disclosure, Transition } from "@headlessui/react";
|
||||
import { observer } from "mobx-react";
|
||||
import { EProductSubscriptionEnum } from "@plane/types";
|
||||
import type { TExploredFeatures } from "@plane/types";
|
||||
import { IconButton } from "@plane/propel/icon-button";
|
||||
import { ChevronRightIcon, GithubIcon, SlackIcon, PiIcon, CloseIcon } from "@plane/propel/icons";
|
||||
import { cn } from "@plane/utils";
|
||||
import { SidebarNavItem } from "@/components/sidebar/sidebar-navigation";
|
||||
import { useUserPermissions } from "@/hooks/store/user";
|
||||
import { useWorkspaceSubscription } from "@/plane-web/hooks/store/use-workspace-subscription";
|
||||
|
||||
type TTrySectionItem = {
|
||||
title: string;
|
||||
icon: React.ReactNode;
|
||||
feature: TExploredFeatures;
|
||||
getHref: (workspaceSlug: string) => string;
|
||||
};
|
||||
|
||||
export const SidebarTrySection = observer(() => {
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
const { workspaceSlug } = useParams();
|
||||
|
||||
const { workspaceInfoBySlug, updateExploredFeatures } = useUserPermissions();
|
||||
const { currentWorkspaceSubscribedPlanDetail: subscriptionDetail } = useWorkspaceSubscription();
|
||||
|
||||
const workspaceSlugStr = workspaceSlug?.toString() ?? "";
|
||||
const currentWorkspaceInfo = workspaceInfoBySlug(workspaceSlugStr);
|
||||
const currentSubscription = subscriptionDetail?.product;
|
||||
|
||||
const trySectionItems = useMemo<TTrySectionItem[]>(
|
||||
() => [
|
||||
{
|
||||
title: "Connect to GitHub",
|
||||
icon: <GithubIcon className="size-4" />,
|
||||
feature: "github_integrated",
|
||||
getHref: (slug: string) => `/${slug}/settings/integrations/`,
|
||||
},
|
||||
{
|
||||
title: "Connect to Slack",
|
||||
icon: <SlackIcon className="size-4" />,
|
||||
feature: "slack_integrated",
|
||||
getHref: (slug: string) => `/${slug}/settings/integrations/`,
|
||||
},
|
||||
{
|
||||
title: "Try Plane AI",
|
||||
icon: <PiIcon className="size-4" />,
|
||||
feature: "ai_chat_tried",
|
||||
getHref: (slug: string) => `/${slug}/pi-chat/`,
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
const unexploredItems = useMemo(() => {
|
||||
if (!currentWorkspaceInfo?.explored_features) return trySectionItems;
|
||||
|
||||
return trySectionItems.filter((item) => !currentWorkspaceInfo.explored_features?.[item.feature]);
|
||||
}, [currentWorkspaceInfo?.explored_features, trySectionItems]);
|
||||
|
||||
const handleTryFeatureClick = useCallback(
|
||||
(feature: TExploredFeatures) => {
|
||||
if (!workspaceSlugStr) return;
|
||||
void updateExploredFeatures(workspaceSlugStr, { [feature]: true });
|
||||
},
|
||||
[workspaceSlugStr, updateExploredFeatures]
|
||||
);
|
||||
|
||||
const toggleOpen = useCallback(() => setIsOpen((prev) => !prev), []);
|
||||
|
||||
// Early returns for when section should not be shown
|
||||
if (unexploredItems.length === 0 || currentSubscription === EProductSubscriptionEnum.FREE) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Disclosure as="div" className="flex flex-col" defaultOpen>
|
||||
<Disclosure.Button
|
||||
className="group w-full flex items-center gap-2 px-2 py-1.5 rounded-sm text-placeholder hover:bg-layer-transparent-hover"
|
||||
onClick={toggleOpen}
|
||||
>
|
||||
<span className="text-13 font-semibold">Try</span>
|
||||
<ChevronRightIcon
|
||||
className={cn("flex-shrink-0 size-3.5 transition-transform", {
|
||||
"rotate-90": isOpen,
|
||||
})}
|
||||
/>
|
||||
</Disclosure.Button>
|
||||
<Transition
|
||||
show={isOpen}
|
||||
enter="transition duration-100 ease-out"
|
||||
enterFrom="transform scale-95 opacity-0"
|
||||
enterTo="transform scale-100 opacity-100"
|
||||
leave="transition duration-75 ease-out"
|
||||
leaveFrom="transform scale-100 opacity-100"
|
||||
leaveTo="transform scale-95 opacity-0"
|
||||
>
|
||||
<Disclosure.Panel as="div" className="flex flex-col gap-1" static>
|
||||
{unexploredItems.map((item) => (
|
||||
<SidebarNavItem key={item.feature} className="group h-7">
|
||||
<Link
|
||||
href={item.getHref(workspaceSlugStr)}
|
||||
className="flex items-center gap-1.5 text-13 font-medium flex-grow text-tertiary"
|
||||
aria-label={item.title}
|
||||
>
|
||||
{item.icon}
|
||||
<span>{item.title}</span>
|
||||
</Link>
|
||||
<IconButton
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
icon={CloseIcon}
|
||||
onClick={() => handleTryFeatureClick(item.feature)}
|
||||
className="group-hover:flex hidden"
|
||||
/>
|
||||
</SidebarNavItem>
|
||||
))}
|
||||
</Disclosure.Panel>
|
||||
</Transition>
|
||||
</Disclosure>
|
||||
);
|
||||
});
|
||||
@@ -33,6 +33,8 @@ import type {
|
||||
IWorkspaceSidebarNavigationItem,
|
||||
IWorkspaceSidebarNavigation,
|
||||
IWorkspaceUserPropertiesResponse,
|
||||
TExploredFeatures,
|
||||
TTips,
|
||||
} from "@plane/types";
|
||||
// services
|
||||
import { APIService } from "@/services/api.service";
|
||||
@@ -132,6 +134,17 @@ export class WorkspaceService extends APIService {
|
||||
});
|
||||
}
|
||||
|
||||
async updateMemberOnboarding(
|
||||
workspaceSlug: string,
|
||||
data: { explored_features?: Partial<Record<TExploredFeatures, boolean>>; tips?: Partial<Record<TTips, boolean>> }
|
||||
): Promise<IWorkspaceMemberMe> {
|
||||
return this.patch(`/api/workspaces/${workspaceSlug}/workspace-member/me/onboarding/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async updateWorkspaceView(workspaceSlug: string, data: { view_props: IWorkspaceViewProps }): Promise<any> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/workspace-views/`, data)
|
||||
.then((response) => response?.data)
|
||||
|
||||
@@ -21,7 +21,14 @@ import {
|
||||
EUserPermissionsLevel,
|
||||
WORKSPACE_SIDEBAR_DYNAMIC_NAVIGATION_ITEMS_LINKS,
|
||||
} from "@plane/constants";
|
||||
import type { EUserProjectRoles, IUserProjectsRole, IWorkspaceMemberMe, TProjectMembership } from "@plane/types";
|
||||
import type {
|
||||
EUserProjectRoles,
|
||||
IUserProjectsRole,
|
||||
IWorkspaceMemberMe,
|
||||
TExploredFeatures,
|
||||
TProjectMembership,
|
||||
TTips,
|
||||
} from "@plane/types";
|
||||
import { EUserWorkspaceRoles } from "@plane/types";
|
||||
// plane web imports
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
@@ -59,6 +66,11 @@ export interface IBaseUserPermissionStore {
|
||||
) => boolean;
|
||||
// actions
|
||||
fetchUserWorkspaceInfo: (workspaceSlug: string) => Promise<IWorkspaceMemberMe>;
|
||||
updateExploredFeatures: (
|
||||
workspaceSlug: string,
|
||||
exploredFeatures: Partial<Record<TExploredFeatures, boolean>>
|
||||
) => Promise<IWorkspaceMemberMe>;
|
||||
updateTips: (workspaceSlug: string, tips: Partial<Record<TTips, boolean>>) => Promise<IWorkspaceMemberMe>;
|
||||
leaveWorkspace: (workspaceSlug: string) => Promise<void>;
|
||||
fetchUserProjectInfo: (workspaceSlug: string, projectId: string) => Promise<TProjectMembership>;
|
||||
fetchUserProjectPermissions: (workspaceSlug: string) => Promise<IUserProjectsRole>;
|
||||
@@ -89,6 +101,8 @@ export abstract class BaseUserPermissionStore implements IBaseUserPermissionStor
|
||||
// computed
|
||||
// actions
|
||||
fetchUserWorkspaceInfo: action,
|
||||
updateExploredFeatures: action,
|
||||
updateTips: action,
|
||||
leaveWorkspace: action,
|
||||
fetchUserProjectInfo: action,
|
||||
fetchUserProjectPermissions: action,
|
||||
@@ -259,6 +273,91 @@ export abstract class BaseUserPermissionStore implements IBaseUserPermissionStor
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Updates the explored features for the current user in a workspace
|
||||
* @param { string } workspaceSlug
|
||||
* @param { Partial<Record<TExploredFeatures, boolean>> } exploredFeatures
|
||||
* @returns { Promise<IWorkspaceMemberMe> }
|
||||
*/
|
||||
updateExploredFeatures = async (
|
||||
workspaceSlug: string,
|
||||
exploredFeatures: Partial<Record<TExploredFeatures, boolean>>
|
||||
): Promise<IWorkspaceMemberMe> => {
|
||||
try {
|
||||
const existingFeatures = this.workspaceUserInfo[workspaceSlug]?.explored_features;
|
||||
const filteredExisting: Partial<Record<TExploredFeatures, boolean>> = {};
|
||||
|
||||
if (existingFeatures) {
|
||||
(Object.keys(existingFeatures) as TExploredFeatures[]).forEach((key) => {
|
||||
const value = existingFeatures[key];
|
||||
if (value !== null) {
|
||||
filteredExisting[key] = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const response = await workspaceService.updateMemberOnboarding(workspaceSlug, {
|
||||
explored_features: {
|
||||
...filteredExisting,
|
||||
...exploredFeatures,
|
||||
},
|
||||
});
|
||||
if (response) {
|
||||
runInAction(() => {
|
||||
set(this.workspaceUserInfo, [workspaceSlug], {
|
||||
...this.workspaceUserInfo[workspaceSlug],
|
||||
explored_features: {
|
||||
...this.workspaceUserInfo[workspaceSlug]?.explored_features,
|
||||
...exploredFeatures,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error("Error updating explored features", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
updateTips = async (workspaceSlug: string, tips: Partial<Record<TTips, boolean>>): Promise<IWorkspaceMemberMe> => {
|
||||
try {
|
||||
const existingTips = this.workspaceUserInfo[workspaceSlug]?.tips;
|
||||
const filteredExisting: Partial<Record<TTips, boolean>> = {};
|
||||
|
||||
if (existingTips) {
|
||||
(Object.keys(existingTips) as TTips[]).forEach((key) => {
|
||||
const value = existingTips[key];
|
||||
if (value !== null) {
|
||||
filteredExisting[key] = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const response = await workspaceService.updateMemberOnboarding(workspaceSlug, {
|
||||
tips: {
|
||||
...filteredExisting,
|
||||
...tips,
|
||||
},
|
||||
});
|
||||
if (response) {
|
||||
runInAction(() => {
|
||||
set(this.workspaceUserInfo, [workspaceSlug], {
|
||||
...this.workspaceUserInfo[workspaceSlug],
|
||||
tips: {
|
||||
...this.workspaceUserInfo[workspaceSlug]?.tips,
|
||||
...tips,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error("Error updating tips", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Leaves a workspace
|
||||
* @param { string } workspaceSlug
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
@@ -91,6 +91,10 @@ export type Properties = {
|
||||
updated_on: boolean;
|
||||
};
|
||||
|
||||
export type TExploredFeatures = "github_integrated" | "slack_integrated" | "ai_chat_tried";
|
||||
|
||||
export type TTips = "mobile_app_download";
|
||||
|
||||
export interface IWorkspaceMember {
|
||||
id: string;
|
||||
member: IUserLite;
|
||||
@@ -120,6 +124,8 @@ export interface IWorkspaceMemberMe {
|
||||
workspace: string;
|
||||
draft_issue_count: number;
|
||||
active_cycles_count: number;
|
||||
explored_features: Record<TExploredFeatures, boolean | null>;
|
||||
tips: Record<TTips, boolean | null>;
|
||||
}
|
||||
|
||||
export interface ILastActiveWorkspaceDetails {
|
||||
|
||||
Reference in New Issue
Block a user