[WEB-2358] chore: optimised the recent collaborators endpoint (#5470)
* chore: optimised the recent collaborators endpoint * chore: recent collabators code refactor * chore: sorted the user's based on active issues * chore: recent collaborators sorting * chore: code refactor --------- Co-authored-by: Anmol Singh Bhatia <anmolsinghbhatia@plane.so>
This commit is contained in:
parent
bf49ebb519
commit
3d7098855f
6 changed files with 49 additions and 320 deletions
|
|
@ -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():
|
||||
|
|
|
|||
15
packages/types/src/dashboard.d.ts
vendored
15
packages/types/src/dashboard.d.ts
vendored
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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<CollaboratorListItemProps> = 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<CollaboratorsListProps> = (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 <WidgetLoader widgetKey={WIDGET_KEY} />;
|
||||
|
||||
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 <WidgetLoader widgetKey={WIDGET_KEY} />;
|
||||
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) => (
|
||||
<div className="mt-7 mb-6 grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 xl:grid-cols-8 gap-2 gap-y-8">
|
||||
{filteredStats?.map((user) => (
|
||||
<CollaboratorListItem
|
||||
key={user.user_id}
|
||||
issueCount={user.active_issue_count}
|
||||
|
|
@ -117,6 +103,6 @@ export const CollaboratorsList: React.FC<CollaboratorsListProps> = (props) => {
|
|||
workspaceSlug={workspaceSlug}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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> = (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(
|
||||
<CollaboratorsList
|
||||
key={i}
|
||||
dashboardId={dashboardId}
|
||||
cursor={`${perPage}:${i}:0`}
|
||||
perPage={perPage}
|
||||
updateResultsCount={updateResultsCount}
|
||||
updateTotalPages={updateTotalPages}
|
||||
workspaceSlug={workspaceSlug}
|
||||
/>
|
||||
);
|
||||
|
||||
const showViewMoreButton = pageCount < totalPages && resultsCount !== 0;
|
||||
const showViewLessButton = pageCount > 1;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mt-7 mb-6 grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 xl:grid-cols-8 gap-2 gap-y-8">
|
||||
{collaboratorsPages}
|
||||
</div>
|
||||
{(showViewLessButton || showViewMoreButton) && (
|
||||
<div className="flex items-center justify-center text-xs w-full">
|
||||
{showViewLessButton && (
|
||||
<Button
|
||||
variant="link-primary"
|
||||
size="sm"
|
||||
className="my-3 hover:bg-custom-primary-100/20"
|
||||
onClick={() => setPageCount(1)}
|
||||
>
|
||||
View less
|
||||
</Button>
|
||||
)}
|
||||
{showViewMoreButton && (
|
||||
<Button
|
||||
variant="link-primary"
|
||||
size="sm"
|
||||
className="my-3 hover:bg-custom-primary-100/20"
|
||||
onClick={handleLoadMore}
|
||||
>
|
||||
View more
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -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<WidgetProps> = (props) => {
|
||||
const { dashboardId, workspaceSlug } = props;
|
||||
|
|
@ -33,16 +30,7 @@ export const RecentCollaboratorsWidget: React.FC<WidgetProps> = (props) => {
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
{searchQuery.trim() !== "" ? (
|
||||
<SearchedCollaboratorsList
|
||||
dashboardId={dashboardId}
|
||||
perPage={PER_PAGE}
|
||||
searchQuery={searchQuery}
|
||||
workspaceSlug={workspaceSlug}
|
||||
/>
|
||||
) : (
|
||||
<DefaultCollaboratorsList dashboardId={dashboardId} perPage={PER_PAGE} workspaceSlug={workspaceSlug} />
|
||||
)}
|
||||
<CollaboratorsList dashboardId={dashboardId} searchQuery={searchQuery} workspaceSlug={workspaceSlug} />
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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> = (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(
|
||||
<CollaboratorsList
|
||||
key={i}
|
||||
dashboardId={dashboardId}
|
||||
cursor={`${perPage}:${i}:0`}
|
||||
perPage={perPage}
|
||||
searchQuery={searchQuery}
|
||||
updateIsLoading={setIsLoading}
|
||||
updateResultsCount={updateResultsCount}
|
||||
updateTotalPages={updateTotalPages}
|
||||
workspaceSlug={workspaceSlug}
|
||||
/>
|
||||
);
|
||||
|
||||
const showViewMoreButton = pageCount < totalPages && resultsCount !== 0;
|
||||
const showViewLessButton = pageCount > 1;
|
||||
|
||||
const emptyStateImage = resolvedTheme === "dark" ? DarkImage : LightImage;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mt-7 mb-6 grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 xl:grid-cols-8 gap-2 gap-y-8">
|
||||
{collaboratorsPages}
|
||||
</div>
|
||||
{!isLoading && totalPages === 0 && (
|
||||
<div className="flex flex-col items-center gap-6 mb-8">
|
||||
<div className="h-24 w-24 flex-shrink-0">
|
||||
<Image src={emptyStateImage} className="w-full h-full" alt="Recent collaborators" />
|
||||
</div>
|
||||
<p className="font-medium text-sm">No matching member</p>
|
||||
</div>
|
||||
)}
|
||||
{(showViewLessButton || showViewMoreButton) && (
|
||||
<div className="flex items-center justify-center text-xs w-full">
|
||||
{showViewLessButton && (
|
||||
<Button
|
||||
variant="link-primary"
|
||||
size="sm"
|
||||
className="my-3 hover:bg-custom-primary-100/20"
|
||||
onClick={() => setPageCount(1)}
|
||||
>
|
||||
View less
|
||||
</Button>
|
||||
)}
|
||||
{showViewMoreButton && (
|
||||
<Button
|
||||
variant="link-primary"
|
||||
size="sm"
|
||||
className="my-3 hover:bg-custom-primary-100/20"
|
||||
onClick={handleLoadMore}
|
||||
>
|
||||
View more
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue