diff --git a/apiserver/plane/app/views/dashboard/base.py b/apiserver/plane/app/views/dashboard/base.py index 5dc1cf1b6..b3e93a403 100644 --- a/apiserver/plane/app/views/dashboard/base.py +++ b/apiserver/plane/app/views/dashboard/base.py @@ -574,105 +574,42 @@ def dashboard_recent_projects(self, request, slug): def dashboard_recent_collaborators(self, request, slug): - # Subquery to count activities for each project member - activity_count_subquery = ( - IssueActivity.objects.filter( - workspace__slug=slug, - actor=OuterRef("member"), - project__project_projectmember__member=request.user, - project__project_projectmember__is_active=True, - project__archived_at__isnull=True, - ) - .values("actor") - .annotate(num_activities=Count("pk")) - .values("num_activities") - ) - - # Get all project members and annotate them with activity counts project_members_with_activities = ( - ProjectMember.objects.filter( + WorkspaceMember.objects.filter( workspace__slug=slug, - project__project_projectmember__member=request.user, - project__project_projectmember__is_active=True, - project__archived_at__isnull=True, is_active=True, ) .annotate( - num_activities=Coalesce( - Subquery(activity_count_subquery), - Value(0), - output_field=IntegerField(), - ), - is_current_user=Case( - When(member=request.user, then=Value(0)), - default=Value(1), - output_field=IntegerField(), + active_issue_count=Count( + Case( + When( + member__issue_assignee__issue__state__group__in=[ + "unstarted", + "started", + ], + member__issue_assignee__issue__workspace__slug=slug, + member__issue_assignee__issue__project__project_projectmember__member=request.user, + member__issue_assignee__issue__project__project_projectmember__is_active=True, + then=F("member__issue_assignee__issue__id"), + ), + distinct=True, + output_field=IntegerField(), + ), + distinct=True, ), + user_id=F("member_id"), ) - .values_list("member", flat=True) - .order_by("is_current_user", "-num_activities") + .values("user_id", "active_issue_count") + .order_by("-active_issue_count") .distinct() ) - search = request.query_params.get("search", None) - if search: - project_members_with_activities = ( - project_members_with_activities.filter( - Q(member__display_name__icontains=search) - | Q(member__first_name__icontains=search) - | Q(member__last_name__icontains=search) - ) - ) - - return self.paginate( - request=request, - queryset=project_members_with_activities, - controller=lambda qs: self.get_results_controller(qs, slug), + return Response( + (project_members_with_activities), + status=status.HTTP_200_OK, ) class DashboardEndpoint(BaseAPIView): - def get_results_controller(self, project_members_with_activities, slug): - user_active_issue_counts = ( - User.objects.filter( - id__in=project_members_with_activities, - ) - .annotate( - active_issue_count=Count( - Case( - When( - issue_assignee__issue__state__group__in=[ - "unstarted", - "started", - ], - issue_assignee__issue__workspace__slug=slug, - issue_assignee__issue__project__project_projectmember__is_active=True, - then=F("issue_assignee__issue__id"), - ), - output_field=IntegerField(), - ), - distinct=True, - ) - ) - .values("active_issue_count", user_id=F("id")) - ) - # Create a dictionary to store the active issue counts by user ID - active_issue_counts_dict = { - user["user_id"]: user["active_issue_count"] - for user in user_active_issue_counts - } - - # Preserve the sequence of project members with activities - paginated_results = [ - { - "user_id": member_id, - "active_issue_count": active_issue_counts_dict.get( - member_id, 0 - ), - } - for member_id in project_members_with_activities - ] - return paginated_results - def create(self, request, slug): serializer = DashboardSerializer(data=request.data) if serializer.is_valid(): diff --git a/packages/types/src/dashboard.d.ts b/packages/types/src/dashboard.d.ts index 9abd1bf22..47e37a6c6 100644 --- a/packages/types/src/dashboard.d.ts +++ b/packages/types/src/dashboard.d.ts @@ -145,17 +145,8 @@ export type TRecentActivityWidgetResponse = IIssueActivity; export type TRecentProjectsWidgetResponse = string[]; export type TRecentCollaboratorsWidgetResponse = { - count: number; - extra_stats: Object | null; - next_cursor: string; - next_page_results: boolean; - prev_cursor: string; - prev_page_results: boolean; - results: { - active_issue_count: number; - user_id: string; - }[]; - total_pages: number; + active_issue_count: number; + user_id: string; }; export type TWidgetStatsResponse = @@ -166,7 +157,7 @@ export type TWidgetStatsResponse = | TCreatedIssuesWidgetResponse | TRecentActivityWidgetResponse[] | TRecentProjectsWidgetResponse - | TRecentCollaboratorsWidgetResponse; + | TRecentCollaboratorsWidgetResponse[]; // dashboard export type TDashboard = { diff --git a/web/core/components/dashboard/widgets/recent-collaborators/collaborators-list.tsx b/web/core/components/dashboard/widgets/recent-collaborators/collaborators-list.tsx index 4bb09311d..bf7b6ac7d 100644 --- a/web/core/components/dashboard/widgets/recent-collaborators/collaborators-list.tsx +++ b/web/core/components/dashboard/widgets/recent-collaborators/collaborators-list.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect } from "react"; +import sortBy from "lodash/sortBy"; import { observer } from "mobx-react"; import Link from "next/link"; import useSWR from "swr"; @@ -52,64 +52,50 @@ const CollaboratorListItem: React.FC = observer((prop }); type CollaboratorsListProps = { - cursor: string; dashboardId: string; - perPage: number; searchQuery?: string; - updateIsLoading?: (isLoading: boolean) => void; - updateResultsCount: (count: number) => void; - updateTotalPages: (count: number) => void; workspaceSlug: string; }; const WIDGET_KEY = "recent_collaborators"; export const CollaboratorsList: React.FC = (props) => { - const { - cursor, - dashboardId, - perPage, - searchQuery = "", - updateIsLoading, - updateResultsCount, - updateTotalPages, - workspaceSlug, - } = props; + const { dashboardId, searchQuery = "", workspaceSlug } = props; // store hooks const { fetchWidgetStats } = useDashboard(); + const { getUserDetails } = useMember(); + const { data: currentUser } = useUser(); const { data: widgetStats } = useSWR( - workspaceSlug && dashboardId && cursor - ? `WIDGET_STATS_${workspaceSlug}_${dashboardId}_${cursor}_${searchQuery}` - : null, - workspaceSlug && dashboardId && cursor + workspaceSlug && dashboardId ? `WIDGET_STATS_${workspaceSlug}_${dashboardId}` : null, + workspaceSlug && dashboardId ? () => fetchWidgetStats(workspaceSlug, dashboardId, { - cursor, - per_page: perPage, - search: searchQuery, widget_key: WIDGET_KEY, }) : null ) as { - data: TRecentCollaboratorsWidgetResponse | undefined; + data: TRecentCollaboratorsWidgetResponse[] | undefined; }; - useEffect(() => { - updateIsLoading?.(true); + if (!widgetStats) return ; - if (!widgetStats) return; + const sortedStats = sortBy(widgetStats, [(user) => user.user_id !== currentUser?.id]); - updateIsLoading?.(false); - updateTotalPages(widgetStats.total_pages); - updateResultsCount(widgetStats.results?.length); - }, [updateIsLoading, updateResultsCount, updateTotalPages, widgetStats]); + const filteredStats = sortedStats.filter((user) => { + const { display_name, first_name, last_name } = getUserDetails(user.user_id) || {}; - if (!widgetStats || !widgetStats?.results) return ; + const searchLower = searchQuery.toLowerCase(); + return ( + display_name?.toLowerCase().includes(searchLower) || + first_name?.toLowerCase().includes(searchLower) || + last_name?.toLowerCase().includes(searchLower) + ); + }); return ( - <> - {widgetStats?.results?.map((user) => ( +
+ {filteredStats?.map((user) => ( = (props) => { workspaceSlug={workspaceSlug} /> ))} - +
); }; diff --git a/web/core/components/dashboard/widgets/recent-collaborators/default-list.tsx b/web/core/components/dashboard/widgets/recent-collaborators/default-list.tsx deleted file mode 100644 index e031df3ee..000000000 --- a/web/core/components/dashboard/widgets/recent-collaborators/default-list.tsx +++ /dev/null @@ -1,76 +0,0 @@ -"use client"; - -import { useState } from "react"; -// components -import { Button } from "@plane/ui"; -import { CollaboratorsList } from "./collaborators-list"; -// ui - -type Props = { - dashboardId: string; - perPage: number; - workspaceSlug: string; -}; - -export const DefaultCollaboratorsList: React.FC = (props) => { - const { dashboardId, perPage, workspaceSlug } = props; - // states - const [pageCount, setPageCount] = useState(1); - const [totalPages, setTotalPages] = useState(0); - const [resultsCount, setResultsCount] = useState(0); - - const handleLoadMore = () => setPageCount((prev) => prev + 1); - - const updateTotalPages = (count: number) => setTotalPages(count); - - const updateResultsCount = (count: number) => setResultsCount(count); - - const collaboratorsPages: JSX.Element[] = []; - for (let i = 0; i < pageCount; i++) - collaboratorsPages.push( - - ); - - const showViewMoreButton = pageCount < totalPages && resultsCount !== 0; - const showViewLessButton = pageCount > 1; - - return ( - <> -
- {collaboratorsPages} -
- {(showViewLessButton || showViewMoreButton) && ( -
- {showViewLessButton && ( - - )} - {showViewMoreButton && ( - - )} -
- )} - - ); -}; diff --git a/web/core/components/dashboard/widgets/recent-collaborators/root.tsx b/web/core/components/dashboard/widgets/recent-collaborators/root.tsx index 65f49a8f4..4d3064207 100644 --- a/web/core/components/dashboard/widgets/recent-collaborators/root.tsx +++ b/web/core/components/dashboard/widgets/recent-collaborators/root.tsx @@ -4,10 +4,7 @@ import { Search } from "lucide-react"; import { Card } from "@plane/ui"; import { WidgetProps } from "@/components/dashboard/widgets"; // components -import { DefaultCollaboratorsList } from "./default-list"; -import { SearchedCollaboratorsList } from "./search-list"; - -const PER_PAGE = 8; +import { CollaboratorsList } from "./collaborators-list"; export const RecentCollaboratorsWidget: React.FC = (props) => { const { dashboardId, workspaceSlug } = props; @@ -33,16 +30,7 @@ export const RecentCollaboratorsWidget: React.FC = (props) => { /> - {searchQuery.trim() !== "" ? ( - - ) : ( - - )} + ); }; diff --git a/web/core/components/dashboard/widgets/recent-collaborators/search-list.tsx b/web/core/components/dashboard/widgets/recent-collaborators/search-list.tsx deleted file mode 100644 index 91a011fa1..000000000 --- a/web/core/components/dashboard/widgets/recent-collaborators/search-list.tsx +++ /dev/null @@ -1,97 +0,0 @@ -"use client"; - -import { useState } from "react"; -import Image from "next/image"; -import { useTheme } from "next-themes"; -// components -// ui -import { Button } from "@plane/ui"; -// assets -import DarkImage from "@/public/empty-state/dashboard/dark/recent-collaborators-1.svg"; -import LightImage from "@/public/empty-state/dashboard/light/recent-collaborators-1.svg"; -import { CollaboratorsList } from "./collaborators-list"; - -type Props = { - dashboardId: string; - perPage: number; - searchQuery: string; - workspaceSlug: string; -}; - -export const SearchedCollaboratorsList: React.FC = (props) => { - const { dashboardId, perPage, searchQuery, workspaceSlug } = props; - // states - const [pageCount, setPageCount] = useState(1); - const [totalPages, setTotalPages] = useState(0); - const [resultsCount, setResultsCount] = useState(0); - const [isLoading, setIsLoading] = useState(true); - // next-themes - const { resolvedTheme } = useTheme(); - - const handleLoadMore = () => setPageCount((prev) => prev + 1); - - const updateTotalPages = (count: number) => setTotalPages(count); - - const updateResultsCount = (count: number) => setResultsCount(count); - - const collaboratorsPages: JSX.Element[] = []; - for (let i = 0; i < pageCount; i++) - collaboratorsPages.push( - - ); - - const showViewMoreButton = pageCount < totalPages && resultsCount !== 0; - const showViewLessButton = pageCount > 1; - - const emptyStateImage = resolvedTheme === "dark" ? DarkImage : LightImage; - - return ( - <> -
- {collaboratorsPages} -
- {!isLoading && totalPages === 0 && ( -
-
- Recent collaborators -
-

No matching member

-
- )} - {(showViewLessButton || showViewMoreButton) && ( -
- {showViewLessButton && ( - - )} - {showViewMoreButton && ( - - )} -
- )} - - ); -};