From 91e44834cbc7eba7caebfaf54182c7538a307a92 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Wed, 21 Jan 2026 19:53:10 +0530 Subject: [PATCH] [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 --- apps/api/plane/app/serializers/__init__.py | 1 + apps/api/plane/app/serializers/workspace.py | 7 + apps/api/plane/app/urls/workspace.py | 7 + apps/api/plane/app/views/__init__.py | 1 + apps/api/plane/app/views/workspace/member.py | 29 ++++ .../[workspaceSlug]/(projects)/sidebar.tsx | 27 ++-- .../components/sidebar/sidebar-wrapper.tsx | 32 ++-- .../workspace/sidebar/sidebar-tip-section.tsx | 103 +++++++++++++ .../workspace/sidebar/sidebar-try-section.tsx | 137 ++++++++++++++++++ apps/web/core/services/workspace.service.ts | 13 ++ .../core/store/user/base-permissions.store.ts | 101 ++++++++++++- .../tips/download-app/download-app-dark.webp | Bin 0 -> 12622 bytes .../tips/download-app/download-app-light.webp | Bin 0 -> 13078 bytes packages/types/src/workspace.ts | 6 + 14 files changed, 438 insertions(+), 26 deletions(-) create mode 100644 apps/web/core/components/workspace/sidebar/sidebar-tip-section.tsx create mode 100644 apps/web/core/components/workspace/sidebar/sidebar-try-section.tsx create mode 100644 apps/web/public/tips/download-app/download-app-dark.webp create mode 100644 apps/web/public/tips/download-app/download-app-light.webp diff --git a/apps/api/plane/app/serializers/__init__.py b/apps/api/plane/app/serializers/__init__.py index 4ef3dc82c4..2b80ac21db 100644 --- a/apps/api/plane/app/serializers/__init__.py +++ b/apps/api/plane/app/serializers/__init__.py @@ -35,6 +35,7 @@ from .workspace import ( WorkspaceHomePreferenceSerializer, StickySerializer, WorkspaceUserMeSerializer, + WorkspaceMemberUserOnboardingSerializer, ) from .project import ( ProjectSerializer, diff --git a/apps/api/plane/app/serializers/workspace.py b/apps/api/plane/app/serializers/workspace.py index ac812b0cb3..f0f8cc0171 100644 --- a/apps/api/plane/app/serializers/workspace.py +++ b/apps/api/plane/app/serializers/workspace.py @@ -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) diff --git a/apps/api/plane/app/urls/workspace.py b/apps/api/plane/app/urls/workspace.py index f074afc255..ad92deea48 100644 --- a/apps/api/plane/app/urls/workspace.py +++ b/apps/api/plane/app/urls/workspace.py @@ -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//workspace-member/me/onboarding/", + WorkspaceMemberUserOnboardingEndpoint.as_view(), + name="workspace-member-onboading", + ), ] diff --git a/apps/api/plane/app/views/__init__.py b/apps/api/plane/app/views/__init__.py index 1caa93648f..86b5f44f51 100644 --- a/apps/api/plane/app/views/__init__.py +++ b/apps/api/plane/app/views/__init__.py @@ -66,6 +66,7 @@ from .workspace.member import ( WorkspaceMemberUserEndpoint, WorkspaceProjectMemberEndpoint, WorkspaceMemberUserViewsEndpoint, + WorkspaceMemberUserOnboardingEndpoint, ) from .workspace.invite import ( WorkspaceInvitationsViewset, diff --git a/apps/api/plane/app/views/workspace/member.py b/apps/api/plane/app/views/workspace/member.py index b565df87a4..640c9a1c70 100644 --- a/apps/api/plane/app/views/workspace/member.py +++ b/apps/api/plane/app/views/workspace/member.py @@ -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 diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/sidebar.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/sidebar.tsx index 6f329975d1..2b44a43eea 100644 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/sidebar.tsx +++ b/apps/web/app/(all)/[workspaceSlug]/(projects)/sidebar.tsx @@ -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 ( }> - {/* Favorites Menu */} - {canPerformWorkspaceMemberActions && !isFavoriteEmpty && } - {/* Teams List */} + {showFavorites && } - {/* Projects List */} ); diff --git a/apps/web/core/components/sidebar/sidebar-wrapper.tsx b/apps/web/core/components/sidebar/sidebar-wrapper.tsx index dbf52fe90a..66c8da6abc 100644 --- a/apps/web/core/components/sidebar/sidebar-wrapper.tsx +++ b/apps/web/core/components/sidebar/sidebar-wrapper.tsx @@ -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(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 ( <> setIsCustomizeNavDialogOpen(false)} /> -
+
{/* Workspace switcher and settings */} @@ -90,13 +101,16 @@ export const SidebarWrapper = observer(function SidebarWrapper(props: TSidebarWr {children} {/* Help Section */} -
- - {/* TODO: To be checked if we need this */} - {/*
- {!shouldRenderAppRail && } - {!isAppRailEnabled && } -
*/} +
+ {isWorkspaceAdmin && ( +
+ + +
+ )} +
+ +
diff --git a/apps/web/core/components/workspace/sidebar/sidebar-tip-section.tsx b/apps/web/core/components/workspace/sidebar/sidebar-tip-section.tsx new file mode 100644 index 0000000000..b9ecfdb5e8 --- /dev/null +++ b/apps/web/core/components/workspace/sidebar/sidebar-tip-section.tsx @@ -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 ( +
+
+ + + +
+ {activeTip.title} + {activeTip.description} +
+
+
+ ); +}); diff --git a/apps/web/core/components/workspace/sidebar/sidebar-try-section.tsx b/apps/web/core/components/workspace/sidebar/sidebar-try-section.tsx new file mode 100644 index 0000000000..1a135c6fe5 --- /dev/null +++ b/apps/web/core/components/workspace/sidebar/sidebar-try-section.tsx @@ -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( + () => [ + { + title: "Connect to GitHub", + icon: , + feature: "github_integrated", + getHref: (slug: string) => `/${slug}/settings/integrations/`, + }, + { + title: "Connect to Slack", + icon: , + feature: "slack_integrated", + getHref: (slug: string) => `/${slug}/settings/integrations/`, + }, + { + title: "Try Plane AI", + icon: , + 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 ( + + + Try + + + + + {unexploredItems.map((item) => ( + + + {item.icon} + {item.title} + + handleTryFeatureClick(item.feature)} + className="group-hover:flex hidden" + /> + + ))} + + + + ); +}); diff --git a/apps/web/core/services/workspace.service.ts b/apps/web/core/services/workspace.service.ts index f250b1ce08..58cf5902e7 100644 --- a/apps/web/core/services/workspace.service.ts +++ b/apps/web/core/services/workspace.service.ts @@ -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>; tips?: Partial> } + ): Promise { + 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 { return this.post(`/api/workspaces/${workspaceSlug}/workspace-views/`, data) .then((response) => response?.data) diff --git a/apps/web/core/store/user/base-permissions.store.ts b/apps/web/core/store/user/base-permissions.store.ts index 10a6b31c65..23f1f2995c 100644 --- a/apps/web/core/store/user/base-permissions.store.ts +++ b/apps/web/core/store/user/base-permissions.store.ts @@ -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; + updateExploredFeatures: ( + workspaceSlug: string, + exploredFeatures: Partial> + ) => Promise; + updateTips: (workspaceSlug: string, tips: Partial>) => Promise; leaveWorkspace: (workspaceSlug: string) => Promise; fetchUserProjectInfo: (workspaceSlug: string, projectId: string) => Promise; fetchUserProjectPermissions: (workspaceSlug: string) => Promise; @@ -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> } exploredFeatures + * @returns { Promise } + */ + updateExploredFeatures = async ( + workspaceSlug: string, + exploredFeatures: Partial> + ): Promise => { + try { + const existingFeatures = this.workspaceUserInfo[workspaceSlug]?.explored_features; + const filteredExisting: Partial> = {}; + + 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>): Promise => { + try { + const existingTips = this.workspaceUserInfo[workspaceSlug]?.tips; + const filteredExisting: Partial> = {}; + + 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 diff --git a/apps/web/public/tips/download-app/download-app-dark.webp b/apps/web/public/tips/download-app/download-app-dark.webp new file mode 100644 index 0000000000000000000000000000000000000000..664981ab10e4fe6fbdbfcbcc34460fc0a7223e9f GIT binary patch literal 12622 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 + tips: Record; } export interface ILastActiveWorkspaceDetails {