diff --git a/apiserver/plane/app/serializers/workspace.py b/apiserver/plane/app/serializers/workspace.py index 1a2b89bba6..f2e8adff09 100644 --- a/apiserver/plane/app/serializers/workspace.py +++ b/apiserver/plane/app/serializers/workspace.py @@ -66,6 +66,8 @@ class WorkSpaceMemberSerializer(DynamicBaseSerializer): class WorkspaceMemberMeSerializer(BaseSerializer): draft_issue_count = serializers.IntegerField(read_only=True) + active_cycles_count = serializers.IntegerField(read_only=True) + class Meta: model = WorkspaceMember fields = "__all__" diff --git a/apiserver/plane/app/views/workspace/member.py b/apiserver/plane/app/views/workspace/member.py index 8ea1a051e3..b22891feb3 100644 --- a/apiserver/plane/app/views/workspace/member.py +++ b/apiserver/plane/app/views/workspace/member.py @@ -7,6 +7,7 @@ from django.db.models import ( Subquery, IntegerField, ) +from django.utils import timezone from django.db.models.functions import Coalesce from django.db.models.functions import Cast @@ -39,6 +40,7 @@ from plane.db.models import ( Workspace, WorkspaceMember, DraftIssue, + Cycle, ) from plane.utils.cache import cache_response, invalidate_cache from plane.payment.bgtasks.member_sync_task import member_sync_task @@ -317,11 +319,26 @@ class WorkspaceMemberUserEndpoint(BaseAPIView): DraftIssue.objects.filter( created_by=request.user, workspace_id=OuterRef("workspace_id"), + project__project_projectmember__member=request.user, + project__project_projectmember__is_active=True, ) .values("workspace_id") .annotate(count=Count("id")) .values("count") ) + active_cycles_count = ( + Cycle.objects.filter( + workspace__slug=slug, + project__project_projectmember__role__gt=5, + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + start_date__lte=timezone.now(), + end_date__gte=timezone.now(), + ) + .values("pk") + .annotate(count=Count("id")) + .values("count") + ) workspace_member = ( WorkspaceMember.objects.filter( @@ -332,6 +349,15 @@ class WorkspaceMemberUserEndpoint(BaseAPIView): Subquery(draft_issue_count, output_field=IntegerField()), 0 ) ) + .annotate( + active_cycles_count=Coalesce( + Subquery( + active_cycles_count, + output_field=IntegerField(), + ), + 0, + ) + ) .first() ) serializer = WorkspaceMemberMeSerializer(workspace_member) diff --git a/packages/types/src/workspace.d.ts b/packages/types/src/workspace.d.ts index 7444161485..fa7f99d83e 100644 --- a/packages/types/src/workspace.d.ts +++ b/packages/types/src/workspace.d.ts @@ -92,6 +92,7 @@ export interface IWorkspaceMemberMe { view_props: IWorkspaceViewProps; workspace: string; draft_issue_count: number; + active_cycles_count: number; } export interface ILastActiveWorkspaceDetails { @@ -222,4 +223,3 @@ export interface IWorkspaceProgressResponse { export interface IWorkspaceAnalyticsResponse { completion_chart: any; } - diff --git a/web/core/components/workspace/sidebar/workspace-menu.tsx b/web/core/components/workspace/sidebar/workspace-menu.tsx index 5654184b14..599e76fe73 100644 --- a/web/core/components/workspace/sidebar/workspace-menu.tsx +++ b/web/core/components/workspace/sidebar/workspace-menu.tsx @@ -24,21 +24,23 @@ import { usePlatformOS } from "@/hooks/use-platform-os"; // plane web components import { UpgradeBadge } from "@/plane-web/components/workspace"; import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; +import { useWorkspaceSubscription } from "@/plane-web/hooks/store"; export const SidebarWorkspaceMenu = observer(() => { // state const [isMenuActive, setIsMenuActive] = useState(false); // refs const actionSectionRef = useRef(null); - // store hooks - const { toggleSidebar, sidebarCollapsed } = useAppTheme(); - const { captureEvent } = useEventTracker(); - const { isMobile } = usePlatformOS(); - const { allowPermissions } = useUserPermissions(); // router params const { workspaceSlug } = useParams(); // pathname const pathname = usePathname(); + // store hooks + const { toggleSidebar, sidebarCollapsed } = useAppTheme(); + const { captureEvent } = useEventTracker(); + const { isMobile } = usePlatformOS(); + const { allowPermissions, workspaceUserInfo } = useUserPermissions(); + const { currentWorkspaceSubscribedPlanDetail } = useWorkspaceSubscription(); // local storage const { setValue: toggleWorkspaceMenu, storedValue } = useLocalStorage("is_workspace_menu_open", true); // derived values @@ -59,6 +61,10 @@ export const SidebarWorkspaceMenu = observer(() => { }, [sidebarCollapsed, toggleWorkspaceMenu]); useOutsideClickDetector(actionSectionRef, () => setIsMenuActive(false)); + const shouldRenderWorkspaceActiveCycle = + workspaceUserInfo[workspaceSlug.toString()]?.active_cycles_count > 0 && + currentWorkspaceSubscribedPlanDetail?.product !== "FREE"; + return ( {!sidebarCollapsed && ( @@ -152,8 +158,9 @@ export const SidebarWorkspaceMenu = observer(() => { })} static > - {SIDEBAR_WORKSPACE_MENU_ITEMS.map( - (link) => + {SIDEBAR_WORKSPACE_MENU_ITEMS.map((link) => { + if (link.key === "active-cycles" && !shouldRenderWorkspaceActiveCycle) return null; + return ( allowPermissions(link.access, EUserPermissionsLevel.WORKSPACE, workspaceSlug.toString()) && ( { ) - )} + ); + })} )} diff --git a/web/core/store/cycle.store.ts b/web/core/store/cycle.store.ts index 204c81d27b..94c7dd4aa3 100644 --- a/web/core/store/cycle.store.ts +++ b/web/core/store/cycle.store.ts @@ -66,6 +66,7 @@ export interface ICycleStore { validateDate: (workspaceSlug: string, projectId: string, payload: CycleDateCheckData) => Promise; setPlotType: (cycleId: string, plotType: TCyclePlotType) => void; setEstimateType: (cycleId: string, estimateType: TCycleEstimateType) => void; + updateWorkspaceUserActiveCycleCount: (workspaceSlug: string, increment: number) => void; // fetch fetchWorkspaceCycles: (workspaceSlug: string) => Promise; fetchAllCycles: (workspaceSlug: string, projectId: string) => Promise; @@ -153,6 +154,7 @@ export class CycleStore implements ICycleStore { removeCycleFromFavorites: action, archiveCycle: action, restoreCycle: action, + updateWorkspaceUserActiveCycleCount: action, }); this.rootStore = _rootStore; @@ -443,6 +445,13 @@ export class CycleStore implements ICycleStore { set(this.estimatedType, [cycleId], estimateType); }; + updateWorkspaceUserActiveCycleCount(workspaceSlug: string, increment: number) { + const workspaceUserInfo = this.rootStore.user.permission.workspaceUserInfo; + const currentCount = workspaceUserInfo[workspaceSlug]?.active_cycles_count ?? 0; + + set(workspaceUserInfo, [workspaceSlug, "active_cycles_count"], currentCount + increment); + } + /** * @description fetch all cycles * @param workspaceSlug @@ -634,6 +643,10 @@ export class CycleStore implements ICycleStore { await this.cycleService.createCycle(workspaceSlug, projectId, data).then((response) => { runInAction(() => { set(this.cycleMap, [response.id], response); + if (response.status?.toLowerCase() === "current") { + // Update workspace active cycle count in workspaceUserInfo + this.updateWorkspaceUserActiveCycleCount(workspaceSlug, 1); + } }); return response; }) @@ -672,6 +685,10 @@ export class CycleStore implements ICycleStore { deleteCycle = async (workspaceSlug: string, projectId: string, cycleId: string) => await this.cycleService.deleteCycle(workspaceSlug, projectId, cycleId).then(() => { runInAction(() => { + if (this.cycleMap[cycleId].status?.toLowerCase() === "current") { + // Update workspace active cycle count in workspaceUserInfo + this.updateWorkspaceUserActiveCycleCount(workspaceSlug, -1); + } delete this.cycleMap[cycleId]; delete this.activeCycleIdMap[cycleId]; if (this.rootStore.favorite.entityMap[cycleId]) this.rootStore.favorite.removeFavoriteFromStore(cycleId); diff --git a/web/ee/components/worklogs/workspace/download/root.tsx b/web/ee/components/worklogs/workspace/download/root.tsx index df60d661a5..e8b65229e0 100644 --- a/web/ee/components/worklogs/workspace/download/root.tsx +++ b/web/ee/components/worklogs/workspace/download/root.tsx @@ -40,6 +40,7 @@ export const WorkspaceWorklogDownloadRoot: FC = o workspaceWorklogDownloadIds && workspaceWorklogDownloadIds.length > 0 ? EWorklogDownloadLoader.MUTATION_LOADER : EWorklogDownloadLoader.INIT_LOADER; + const worklogDownloadIds = worklogDownloadIdsByWorkspaceId(workspaceId) || []; // fetching workspace worklog downloads useSWR(workspaceSlug ? `WORKSPACE_WORKLOG_DOWNLOADS_${workspaceSlug}` : null, () => @@ -50,6 +51,8 @@ export const WorkspaceWorklogDownloadRoot: FC = o if (loader === EWorklogDownloadLoader.INIT_LOADER) return ; + if (worklogDownloadIds.length <= 0) return <>; + return (
diff --git a/web/ee/store/cycle.store.ts b/web/ee/store/cycle.store.ts index a853f6ba6d..d5e66485e2 100644 --- a/web/ee/store/cycle.store.ts +++ b/web/ee/store/cycle.store.ts @@ -194,6 +194,10 @@ export class CycleStore extends CeCycleStore implements ICycleStore { return await this.cycleService.createCycle(workspaceSlug, projectId, { ...data, version }).then((response) => { runInAction(() => { set(this.cycleMap, [response.id], response); + if (response.status?.toLowerCase() === "current") { + // Update workspace active cycle count in workspaceUserInfo + this.updateWorkspaceUserActiveCycleCount(workspaceSlug, 1); + } }); return response; });