[WEB-2932] chore: member validation and improvement (#2139)

* feat: add member validation

* fix: member validation

* fix: remove workspace guests from all team level dropdowns

* chore: add or remove members from create update modal

* chore: team member role update

* chore: remove team pages/ views favorite option

* chore: minor UI fixes

* fix: team member statistics

* chore: update members leave/ role update error message

* chore: remove member validation for activity

* chore: member removal

---------

Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
This commit is contained in:
Prateek Shourya
2025-01-07 20:33:47 +05:30
committed by GitHub
parent 1d0df47135
commit cc3a1ca054
11 changed files with 233 additions and 186 deletions

View File

@@ -3,6 +3,7 @@ from rest_framework.response import Response
from rest_framework import status
import json
# Django imports
from django.utils import timezone
from django.core.serializers.json import DjangoJSONEncoder
@@ -21,13 +22,14 @@ from plane.app.permissions import (
ProjectLitePermission,
WorkspaceUserPermission,
)
from plane.db.models import Project, ProjectMember, IssueUserProperty, WorkspaceMember
from plane.ee.models import TeamSpaceMember, TeamSpaceProject
from plane.bgtasks.project_add_user_email_task import project_add_user_email
from plane.utils.host import base_host
from plane.app.permissions.base import allow_permission, ROLE
from plane.ee.bgtasks.project_activites_task import project_activity
class ProjectMemberViewSet(BaseViewSet):
serializer_class = ProjectMemberAdminSerializer
model = ProjectMember
@@ -55,6 +57,35 @@ class ProjectMemberViewSet(BaseViewSet):
.select_related("workspace", "workspace__owner")
)
def get_team_error_message(
self,
is_singular,
team_count,
type,
project_member_id=None,
requested_project_member_id=None,
):
if type == "update_role":
return (
"This project is linked to a team they are in. Remove them from that team first."
if is_singular
else f"This project is linked to {team_count} teams they are in. Remove them from those teams first."
)
return (
(
"This project is linked to a team you are in. Remove yourself from that team first."
if is_singular
else f"This project is linked to {team_count} teams you are in. Remove yourself from those teams first."
)
if str(project_member_id) == str(requested_project_member_id)
else (
"This project is linked to a team they are in. Remove them from that team first."
if is_singular
else f"This project is linked to {team_count} teams they are in. Remove them from those teams first."
)
)
@allow_permission([ROLE.ADMIN])
def create(self, request, slug, project_id):
# Get the list of members to be added to the project and their roles i.e. the user_id and the role
@@ -245,6 +276,28 @@ class ProjectMemberViewSet(BaseViewSet):
status=status.HTTP_400_BAD_REQUEST,
)
team_ids = TeamSpaceMember.objects.filter(
workspace__slug=slug, member_id=project_member.member_id
).values_list("team_space_id", flat=True)
team_project_ids = TeamSpaceProject.objects.filter(
project_id=project_id, team_space_id__in=team_ids
).values_list("team_space_id", flat=True)
if (
"role" in request.data
and team_project_ids.exists()
and int(request.data.get("role")) < 10
):
team_count = team_project_ids.count()
is_singular = team_count == 1
return Response(
{
"error": self.get_team_error_message(
is_singular, team_count, "update_role"
)
},
status=status.HTTP_400_BAD_REQUEST,
)
serializer = ProjectMemberSerializer(
project_member, data=request.data, partial=True
)
@@ -285,6 +338,32 @@ class ProjectMemberViewSet(BaseViewSet):
status=status.HTTP_400_BAD_REQUEST,
)
# Check if this user is part of any team which is part of this project
team_ids = TeamSpaceMember.objects.filter(
workspace__slug=slug, member_id=project_member.member_id
).values_list("team_space_id", flat=True)
team_project_ids = TeamSpaceProject.objects.filter(
project_id=project_id, team_space_id__in=team_ids
).values_list("team_space_id", flat=True)
# Check if this project is part of any team
if team_project_ids.exists():
team_count = team_project_ids.count()
is_singular = team_count == 1
return Response(
{
"error": self.get_team_error_message(
is_singular,
team_count,
"leave",
project_member.id,
requesting_project_member.id,
)
},
status=status.HTTP_400_BAD_REQUEST,
)
project_member.is_active = False
project_member.save()
project_activity.delay(
@@ -293,11 +372,8 @@ class ProjectMemberViewSet(BaseViewSet):
actor_id=str(request.user.id),
project_id=str(project_id),
current_instance=json.dumps(
{
"members": [str(project_member.member_id)],
"removed": True
}
),
{"members": [str(project_member.member_id)], "removed": True}
),
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
@@ -323,10 +399,40 @@ class ProjectMemberViewSet(BaseViewSet):
):
return Response(
{
"error": "You cannot leave the project as your the only admin of the project you will have to either delete the project or create an another admin"
"error": """
You cannot leave the project as your the only admin of the project
you will have to either delete the project or create an another admin
"""
},
status=status.HTTP_400_BAD_REQUEST,
)
# Check if this user is part of any team which is part of this project
team_ids = TeamSpaceMember.objects.filter(
workspace__slug=slug, member_id=request.user.id
).values_list("team_space_id", flat=True)
team_project_ids = TeamSpaceProject.objects.filter(
project_id=project_id, team_space_id__in=team_ids
).values_list("team_space_id", flat=True)
# Check if this project is part of any team
if team_project_ids.exists():
team_count = team_project_ids.count()
is_singular = team_count == 1
return Response(
{
"error": self.get_team_error_message(
is_singular,
team_count,
"leave",
project_member.id,
project_member.id,
)
},
status=status.HTTP_400_BAD_REQUEST,
)
# Deactivate the user
project_member.is_active = False
project_member.save()
@@ -336,11 +442,8 @@ class ProjectMemberViewSet(BaseViewSet):
actor_id=str(request.user.id),
project_id=str(project_id),
current_instance=json.dumps(
{
"members": [str(request.user.id)],
"removed": False
}
),
{"members": [str(request.user.id)], "removed": False}
),
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),

View File

@@ -14,7 +14,12 @@ from rest_framework.response import Response
# Module imports
from plane.db.models import Cycle, Issue, IssueView, Page, IssueRelation, IssueAssignee
from plane.ee.models import TeamSpacePage, TeamSpaceProject, TeamSpaceView
from plane.ee.models import (
TeamSpacePage,
TeamSpaceProject,
TeamSpaceView,
TeamSpaceMember,
)
from plane.ee.permissions import TeamSpacePermission, WorkspaceUserPermission
from plane.payment.flags.flag import FeatureFlag
from plane.payment.flags.flag_decorator import check_feature_flag
@@ -341,7 +346,12 @@ class TeamSpaceStatisticsEndpoint(TeamBaseEndpoint):
)
return Response(issue_map, status=status.HTTP_200_OK)
def member_tree(self, project_ids, filters):
def member_tree(self, team_space_id, project_ids, filters):
# Get all team member ids
team_member_ids = TeamSpaceMember.objects.filter(
team_space_id=team_space_id
).values_list("member_id", flat=True)
issue_ids = (
Issue.issue_objects.filter(project_id__in=project_ids)
.filter(**filters)
@@ -349,7 +359,9 @@ class TeamSpaceStatisticsEndpoint(TeamBaseEndpoint):
)
# Get the issue map:
issue_map = (
IssueAssignee.objects.filter(issue__in=issue_ids)
IssueAssignee.objects.filter(
issue__in=issue_ids, assignee_id__in=team_member_ids
)
.annotate(identifier=F("assignee_id"))
.values("identifier")
.annotate(count=Count("id"))
@@ -372,4 +384,4 @@ class TeamSpaceStatisticsEndpoint(TeamBaseEndpoint):
if data_key == "projects":
return self.project_tree(project_ids, filters)
else:
return self.member_tree(project_ids, filters)
return self.member_tree(team_space_id, project_ids, filters)

View File

@@ -105,13 +105,14 @@ class TeamSpaceMembersEndpoint(TeamBaseEndpoint):
team_space = TeamSpace.objects.get(workspace__slug=slug, pk=team_space_id)
# Get the current team space members
current_team_space_members = list(
TeamSpaceMember.objects.filter(
current_members = list(
str(member_id) for member_id in TeamSpaceMember.objects.filter(
workspace__slug=slug, team_space_id=team_space_id
).values_list("member_id", flat=True)
)
current_instance = json.dumps(
{"member_ids": current_team_space_members}, cls=DjangoJSONEncoder
{"member_ids": current_members}, cls=DjangoJSONEncoder
)
# Get the list of team space members
@@ -123,9 +124,20 @@ class TeamSpaceMembersEndpoint(TeamBaseEndpoint):
.order_by("sort_order")
)
# Set of newly added members
added_members = set(member_ids) - set(current_members)
# Set of dropped members
dropped_members = set(current_members) - set(member_ids)
# Remove dropped members from team
TeamSpaceMember.objects.filter(
workspace__slug=slug, team_space_id=team_space_id, member_id__in=dropped_members
).delete()
# Create team space members
bulk_team_space_members = []
for member in member_ids:
for member in added_members:
sort_order = next(
(
member.get("sort_order")
@@ -157,11 +169,6 @@ class TeamSpaceMembersEndpoint(TeamBaseEndpoint):
workspace__slug=slug, team_space_id=team_space_id
)
# add current members also to requested data to make it consistent
request.data["member_ids"] = [
str(mem) for mem in current_team_space_members
] + request.data.get("member_ids", [])
# Create activity
team_space_activity.delay(
type="team_space.activity.updated",

View File

@@ -1,4 +1,5 @@
import { FC, useEffect, useMemo, useState } from "react";
import uniq from "lodash/uniq";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { ChevronDown, Plus, X } from "lucide-react";
@@ -17,7 +18,8 @@ import {
import { getFileURL } from "@/helpers/file.helper";
// hooks
import { useMember } from "@/hooks/store";
// plane web hooks
// plane web imports
import { EUserPermissions } from "@/plane-web/constants";
import { useTeams } from "@/plane-web/hooks/store";
type Props = {
@@ -32,51 +34,53 @@ export const AddTeamMembersModal: FC<Props> = observer((props) => {
const { workspaceSlug } = useParams();
// states
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
const [teamMemberIds, setTeamMemberIds] = useState<string[]>([]);
const [memberIdsToInvite, setMemberIdsToInvite] = useState<string[]>([]);
// store hooks
const {
workspace: { workspaceMemberIds, getWorkspaceMemberDetails },
} = useMember();
const { getTeamById, getTeamMemberIds, addTeamMembers } = useTeams();
const { getTeamById, getTeamMemberIds, updateTeamMembers } = useTeams();
// derived values
const teamDetail = teamId ? getTeamById(teamId) : undefined;
const memberIds = teamId ? getTeamMemberIds(teamId) : [];
const teamMemberIds = teamId ? getTeamMemberIds(teamId) : [];
const uninvitedPeople = useMemo(
() =>
workspaceMemberIds?.filter((userId) => {
const isInvited = memberIds?.find((u) => u === userId) || teamMemberIds?.find((u) => u === userId);
const memberDetails = getWorkspaceMemberDetails(userId);
if (memberDetails?.role === EUserPermissions.GUEST) return false;
const isInvited = teamMemberIds?.find((u) => u === userId) || memberIdsToInvite?.find((u) => u === userId);
return !isInvited;
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[teamMemberIds]
[memberIdsToInvite]
);
const isButtonDisabled = useMemo(
() => teamMemberIds.length === 0 || teamMemberIds.some((id) => !id),
[teamMemberIds]
() => memberIdsToInvite.length === 0 || memberIdsToInvite.some((id) => !id),
[memberIdsToInvite]
);
useEffect(() => {
if (isModalOpen) {
if (teamDetail) {
setTeamMemberIds([""]);
setMemberIdsToInvite([""]);
} else {
setTeamMemberIds([""]);
setMemberIdsToInvite([""]);
}
}
}, [teamDetail, isModalOpen]);
const handleModalClearAndClose = () => {
setTeamMemberIds([]);
setMemberIdsToInvite([]);
handleModalClose();
};
const handleAddTeamMembers = async () => {
if (!teamId || !teamMemberIds) return;
if (!teamId || !memberIdsToInvite) return;
// clean up team member ids
setTeamMemberIds(teamMemberIds.filter((id) => id !== ""));
setMemberIdsToInvite(memberIdsToInvite.filter((id) => id !== ""));
setIsSubmitting(true);
await addTeamMembers(workspaceSlug?.toString(), teamId, teamMemberIds)
await updateTeamMembers(workspaceSlug?.toString(), teamId, uniq([...(teamMemberIds ?? []), ...memberIdsToInvite]))
.then(() => {
handleModalClearAndClose();
setToast({
@@ -140,7 +144,7 @@ export const AddTeamMembersModal: FC<Props> = observer((props) => {
<div className="space-y-5">
<h3 className="text-lg font-medium leading-6 text-custom-text-100">Add coworkers, clients, and consultants</h3>
<div className="mb-3 space-y-4">
{teamMemberIds.map((memberId, index) => {
{memberIdsToInvite.map((memberId, index) => {
const selectedMember = getWorkspaceMemberDetails(memberId);
return (
<div key={memberId} className="group mb-1 flex items-center justify-between gap-x-4 text-sm w-full">
@@ -164,11 +168,11 @@ export const AddTeamMembersModal: FC<Props> = observer((props) => {
</button>
}
onChange={(val: string) => {
setTeamMemberIds(
teamMemberIds.map((_, i) => {
setMemberIdsToInvite(
memberIdsToInvite.map((_, i) => {
if (i === index) {
// Don't add if value already exists in array
if (teamMemberIds.includes(val)) return "";
if (memberIdsToInvite.includes(val)) return "";
return val;
}
return _;
@@ -180,12 +184,12 @@ export const AddTeamMembersModal: FC<Props> = observer((props) => {
/>
</div>
<div className="flex items-center justify-between gap-2 flex-shrink-0 ">
{teamMemberIds.length > 1 && (
{memberIdsToInvite.length > 1 && (
<div className="flex-item flex w-6">
<button
type="button"
className="place-items-center self-center rounded"
onClick={() => setTeamMemberIds(teamMemberIds.filter((_, i) => i !== index))}
onClick={() => setMemberIdsToInvite(memberIdsToInvite.filter((_, i) => i !== index))}
>
<X className="h-4 w-4 text-custom-text-200" />
</button>
@@ -201,7 +205,7 @@ export const AddTeamMembersModal: FC<Props> = observer((props) => {
<button
type="button"
className="flex items-center gap-1.5 bg-transparent py-2 pr-3 text-sm font-medium text-custom-primary outline-custom-primary"
onClick={() => setTeamMemberIds([...teamMemberIds, ""])}
onClick={() => setMemberIdsToInvite([...memberIdsToInvite, ""])}
>
<Plus className="h-4 w-4" />
Add another
@@ -219,8 +223,8 @@ export const AddTeamMembersModal: FC<Props> = observer((props) => {
onClick={handleAddTeamMembers}
>
{isSubmitting
? `${teamMemberIds && teamMemberIds.length > 1 ? "Adding members" : "Adding member"}`
: `${teamMemberIds && teamMemberIds.length > 1 ? "Add members" : "Add member"}`}
? `${memberIdsToInvite && memberIdsToInvite.length > 1 ? "Adding members" : "Adding member"}`
: `${memberIdsToInvite && memberIdsToInvite.length > 1 ? "Add members" : "Add member"}`}
</Button>
</div>
</div>

View File

@@ -1,14 +1,13 @@
"use client";
import { FormEvent, useState } from "react";
import { FormEvent, useMemo, useState } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { InfoIcon } from "lucide-react";
// types
import { TTeam } from "@plane/types";
import { EFileAssetType } from "@plane/types/src/enums";
// ui
import { Button, CustomEmojiIconPicker, Input, Logo, Tooltip } from "@plane/ui";
import { Button, CustomEmojiIconPicker, Input, Logo } from "@plane/ui";
// components
import { MemberDropdown, ProjectDropdown } from "@/components/dropdowns";
import { RichTextEditor } from "@/components/editor";
@@ -17,8 +16,9 @@ import { cn } from "@/helpers/common.helper";
import { convertHexEmojiToDecimal } from "@/helpers/emoji.helper";
import { getDescriptionPlaceholder } from "@/helpers/issue.helper";
// store hooks
import { useWorkspace } from "@/hooks/store";
import { useMember, useWorkspace } from "@/hooks/store";
// plane web components
import { EUserPermissions } from "@/plane-web/constants";
import { useTeams } from "@/plane-web/hooks/store";
import { useEditorMentionSearch } from "@/plane-web/hooks/use-editor-mention-search";
// services
@@ -44,9 +44,20 @@ export const CreateOrUpdateTeamForm: React.FC<Props> = observer((props) => {
const [errors, setErrors] = useState<{ name?: string }>({});
// store hooks
const { currentWorkspace } = useWorkspace();
const {
workspace: { workspaceMemberIds, getWorkspaceMemberDetails },
} = useMember();
const { getTeamMemberIds } = useTeams();
// derived values
const teamMemberIds = teamDetail?.id ? (getTeamMemberIds(teamDetail.id) ?? []) : (formData?.member_ids ?? []);
const userIdsWithAdminOrMemberRole = useMemo(
() =>
workspaceMemberIds?.filter((userId) => {
const memberDetails = getWorkspaceMemberDetails(userId);
return memberDetails?.role === EUserPermissions.GUEST ? false : true;
}),
[workspaceMemberIds, getWorkspaceMemberDetails]
);
// use editor mention search
const { searchEntity } = useEditorMentionSearch({
memberIds: teamMemberIds,
@@ -161,6 +172,7 @@ export const CreateOrUpdateTeamForm: React.FC<Props> = observer((props) => {
<p className="text-sm text-custom-text-300">Team lead</p>
<MemberDropdown
value={formData.lead_id ?? ""}
memberIds={userIdsWithAdminOrMemberRole}
onChange={(val) => {
if (val && val !== formData.lead_id) {
handleFormDataChange("lead_id", val);
@@ -180,21 +192,12 @@ export const CreateOrUpdateTeamForm: React.FC<Props> = observer((props) => {
/>
</div>
<div className="space-y-0.5">
<p className="flex gap-1.5 items-center text-sm text-custom-text-300">
<span>{teamDetail?.id ? "Add team members" : "Team members"}</span>
{teamDetail?.id && (
<Tooltip
position="right"
tooltipContent="Team members cannot be removed directly from here. Please visit the team details page sidebar to manage removals."
>
<InfoIcon className="size-3.5 text-custom-text-400 hover:text-custom-text-300 cursor-help outline-none" />
</Tooltip>
)}
</p>
<p className="flex gap-1.5 items-center text-sm text-custom-text-300">Team members</p>
<MemberDropdown
value={formData.member_ids ?? []}
memberIds={userIdsWithAdminOrMemberRole}
onChange={(val) => {
handleFormDataChange("member_ids", Array.from(new Set([...(teamDetail?.member_ids ?? []), ...val])));
handleFormDataChange("member_ids", val);
}}
multiple
buttonVariant="border-with-text"

View File

@@ -11,6 +11,8 @@ import { Props } from "@/components/icons/types";
// hooks
import { cn } from "@/helpers/common.helper";
// plane web hooks
import { useMember } from "@/hooks/store";
import { EUserPermissions } from "@/plane-web/constants";
import { useTeams } from "@/plane-web/hooks/store";
// local components
import { TeamsPropertiesList } from "./list";
@@ -34,6 +36,9 @@ export const TeamsOverviewSidebarProperties = observer((props: TTeamsOverviewSid
// router
const { workspaceSlug } = useParams();
// hooks
const {
workspace: { workspaceMemberIds, getWorkspaceMemberDetails },
} = useMember();
const { getTeamById, getTeamEntitiesById, getTeamEntitiesLoaderById, updateTeam } = useTeams();
// derived values
const team = getTeamById(teamId);
@@ -41,6 +46,14 @@ export const TeamsOverviewSidebarProperties = observer((props: TTeamsOverviewSid
const teamEntitiesLoader = getTeamEntitiesLoaderById(teamId);
const linkedEntitiesCount = teamEntities?.linked_entities.total;
const teamEntitiesCount = teamEntities?.team_entities.total;
const userIdsWithAdminOrMemberRole = useMemo(
() =>
workspaceMemberIds?.filter((userId) => {
const memberDetails = getWorkspaceMemberDetails(userId);
return memberDetails?.role === EUserPermissions.GUEST ? false : true;
}),
[workspaceMemberIds, getWorkspaceMemberDetails]
);
if (!team) return null;
@@ -64,6 +77,7 @@ export const TeamsOverviewSidebarProperties = observer((props: TTeamsOverviewSid
value: (
<MemberDropdown
value={team.lead_id ?? null}
memberIds={userIdsWithAdminOrMemberRole}
onChange={handleTeamLeadChange}
multiple={false}
buttonVariant="transparent-with-text"
@@ -78,7 +92,7 @@ export const TeamsOverviewSidebarProperties = observer((props: TTeamsOverviewSid
),
},
],
[handleTeamLeadChange, team.lead_id, isEditingAllowed]
[handleTeamLeadChange, team.lead_id, userIdsWithAdminOrMemberRole, isEditingAllowed]
);
const LINKED_ENTITIES: TPropertyListItem[] = useMemo(

View File

@@ -1,101 +0,0 @@
import { observer } from "mobx-react";
import { TriangleAlert } from "lucide-react";
// plane imports
import { cn } from "@plane/utils";
// common classNames
const ICON_CLASSNAME = "size-2.5 rounded-full";
const TITLE_CLASSNAME = "font-light text-custom-text-200";
const ITEM_CONTAINER_CLASSNAME = "py-1";
const VALUE_CLASSNAME = "font-semibold text-custom-text-300";
type TWorkloadDetailItemProps = {
title: string;
icon?: React.ReactNode;
value?: React.ReactNode;
containerClassName?: string;
titleClassName?: string;
valueClassName?: string;
};
const WorkloadDetailItem = (props: TWorkloadDetailItemProps) => {
const { title, icon, value, containerClassName, titleClassName, valueClassName } = props;
return (
<div className={cn("flex items-center gap-2 text-sm", containerClassName)}>
<span className={cn("grow flex gap-2 items-center", titleClassName)}>
{icon}
<span>{title}</span>
</span>
<span className={cn("flex-shrink-0 flex items-center", valueClassName)}>{value}</span>
</div>
);
};
type TTeamWorkloadDetailProps = {
teamId: string;
};
export const TeamWorkloadDetail: React.FC<TTeamWorkloadDetailProps> = observer((props) => {
const { teamId } = props;
return (
<div className="w-full h-full">
<h4 className="text-sm text-custom-text-300 font-medium">Summary of team&apos;s issues</h4>
<div className="flex flex-col gap-2.5">
<div className="flex items-center gap-2 py-2.5 border-b border-custom-border-100 text-[#FF9500]">
<TriangleAlert size={16} />
<span>12 issues are not assigned due date</span>
</div>
<div className="flex flex-col gap-2 border-b border-custom-border-100 px-2 pb-2">
<WorkloadDetailItem title="States on chart" value="Issues" containerClassName="text-custom-text-300" />
<WorkloadDetailItem
title="Overdue"
icon={<span className={cn(ICON_CLASSNAME, "bg-[#FFCCCC]")} />}
value="12"
titleClassName={TITLE_CLASSNAME}
containerClassName={ITEM_CONTAINER_CLASSNAME}
valueClassName={cn(VALUE_CLASSNAME, "bg-red-500 text-white px-0.5 rounded")}
/>
<WorkloadDetailItem
title="Pending"
icon={<span className={cn(ICON_CLASSNAME, "bg-custom-background-80/80")} />}
value={
<>
<div className={"flex-shrink-0 absolute -top-0.5 -right-0.5 size-1.5 bg-red-500 rounded-full"} />
120
</>
}
titleClassName={TITLE_CLASSNAME}
containerClassName={ITEM_CONTAINER_CLASSNAME}
valueClassName={cn(VALUE_CLASSNAME, "relative bg-custom-background-80/80 px-0.5 rounded")}
/>
<WorkloadDetailItem
title="Completed"
icon={<span className={cn(ICON_CLASSNAME, "bg-[#004EFF]")} />}
value="140"
titleClassName={TITLE_CLASSNAME}
containerClassName={ITEM_CONTAINER_CLASSNAME}
valueClassName={cn(VALUE_CLASSNAME)}
/>
</div>
<div className="flex flex-col gap-2 lg:border-b border-custom-border-100 px-2 pb-2">
<WorkloadDetailItem title="Other states" containerClassName="text-custom-text-300" />
<WorkloadDetailItem
title="Backlog"
value="14"
titleClassName={TITLE_CLASSNAME}
containerClassName={ITEM_CONTAINER_CLASSNAME}
valueClassName={cn(VALUE_CLASSNAME)}
/>
<WorkloadDetailItem
title="Cancelled"
value="3"
titleClassName={TITLE_CLASSNAME}
containerClassName={ITEM_CONTAINER_CLASSNAME}
valueClassName={cn(VALUE_CLASSNAME)}
/>
</div>
</div>
</div>
);
});

View File

@@ -61,7 +61,7 @@ export const TeamWorkloadSummary: React.FC<TTeamWorkloadSummaryProps> = observer
{isUpdating && <Spinner size={12} className="animate-spin flex-shrink-0" />}
</div>
<div className="flex flex-col gap-2.5">
{teamWorkloadSummary && teamWorkloadSummary.no_due_date_issues > 0 ? (
{teamWorkloadSummary ? (
<div className="flex items-center gap-1.5 py-2.5 border-b border-custom-border-100 text-sm font-medium text-[#FF9500]">
<TriangleAlert size={16} strokeWidth={2} />
<span>{teamWorkloadSummary.no_due_date_issues} issues are not assigned due date</span>

View File

@@ -58,13 +58,13 @@ export const TeamViewListItemAction: FC<Props> = observer((props) => {
// handlers
const handleAddToFavorites = () => {
if (!workspaceSlug || !teamId) return;
if (!workspaceSlug || !teamId || view.is_team_view) return;
addViewToFavorites(workspaceSlug.toString(), teamId.toString(), view.id);
};
const handleRemoveFromFavorites = () => {
if (!workspaceSlug || !teamId) return;
if (!workspaceSlug || !teamId || view.is_team_view) return;
removeViewFromFavorites(workspaceSlug.toString(), teamId.toString(), view.id);
};
@@ -96,7 +96,7 @@ export const TeamViewListItemAction: FC<Props> = observer((props) => {
{/* created by */}
{<ButtonAvatars showTooltip={false} userIds={ownedByDetails?.id ?? []} />}
{isEditingAllowed && (
{isEditingAllowed && !view.is_team_view && (
<FavoriteStar
onClick={(e) => {
e.preventDefault();

View File

@@ -143,8 +143,9 @@ export class TeamPage extends BasePage implements TTeamPage {
* @description returns true if the current logged in user can favorite the page
*/
get canCurrentUserFavoritePage() {
const userRole = this.getUserWorkspaceRole();
return !!userRole && userRole >= EUserPermissions.MEMBER;
return false;
// const userRole = this.getUserWorkspaceRole();
// return !!userRole && userRole >= EUserPermissions.MEMBER;
}
/**

View File

@@ -1,4 +1,5 @@
import set from "lodash/set";
import uniq from "lodash/uniq";
import update from "lodash/update";
import xor from "lodash/xor";
import { makeObservable, action, computed, observable, runInAction } from "mobx";
@@ -50,7 +51,7 @@ export interface ITeamStore {
// CURD actions
createTeam: (workspaceSlug: string, data: Partial<TTeam>) => Promise<TTeam>;
updateTeam: (workspaceSlug: string, teamId: string, data: Partial<TTeam>) => Promise<TTeam>;
addTeamMembers: (
updateTeamMembers: (
workspaceSlug: string,
teamId: string,
memberIds: string[],
@@ -100,7 +101,7 @@ export class TeamStore implements ITeamStore {
// CURD actions
createTeam: action,
updateTeam: action,
addTeamMembers: action,
updateTeamMembers: action,
removeTeamMember: action,
joinTeam: action,
deleteTeam: action,
@@ -378,8 +379,8 @@ export class TeamStore implements ITeamStore {
// create team
const team = await this.teamService.createTeam(workspaceSlug, data);
// add team members
if (data.member_ids) {
await this.addTeamMembers(workspaceSlug, team.id, data.member_ids, false);
if (data.member_ids && data.member_ids.length > 0) {
await this.updateTeamMembers(workspaceSlug, team.id, data.member_ids, false);
}
// set team map along with member_ids
runInAction(() => {
@@ -407,21 +408,20 @@ export class TeamStore implements ITeamStore {
const teamDetailsBeforeUpdate = this.getTeamById(teamId);
// remove member_ids from data
const { member_ids, ...payload } = data;
// get member ids to be added
const memberIdsToBeAdded = member_ids
? member_ids.filter((memberId) => !teamDetailsBeforeUpdate.member_ids?.includes(memberId))
: [];
// Check if team projects have been updated
const areProjectsUpdated =
!!data.project_ids && xor(teamDetailsBeforeUpdate.project_ids, data.project_ids).length > 0;
// Check if team members have been updated
const areMembersUpdated = !!member_ids && xor(teamDetailsBeforeUpdate.member_ids, member_ids).length > 0;
try {
// update team map
runInAction(() => {
update(this.teamMap, [teamId], (team) => ({ ...team, ...data }));
});
// add team members if any
if (memberIdsToBeAdded.length > 0) {
await this.addTeamMembers(workspaceSlug, teamId, memberIdsToBeAdded, false);
if (areMembersUpdated) {
await this.updateTeamMembers(workspaceSlug, teamId, member_ids, false);
}
// update team
const team = await this.teamService.updateTeam(workspaceSlug, teamId, payload);
@@ -445,14 +445,14 @@ export class TeamStore implements ITeamStore {
};
/**
* Adds team members to a team and updates the store
* Updates team members of a team and updates the store
* @param workspaceSlug
* @param teamId
* @param memberIds
* @param updateTeamDetails
* @returns Promise<TTeamMember[]>
*/
addTeamMembers = async (
updateTeamMembers = async (
workspaceSlug: string,
teamId: string,
memberIds: string[],
@@ -461,8 +461,10 @@ export class TeamStore implements ITeamStore {
try {
// add team members
const teamMembers = await this.teamService.addTeamMembers(workspaceSlug, teamId, memberIds);
// set team members map
runInAction(() => {
// clear team members map
set(this.teamMembersMap, [teamId], {});
// set team members map
teamMembers.forEach((member) => {
set(this.teamMembersMap, [teamId, member.member], member);
});
@@ -529,8 +531,10 @@ export class TeamStore implements ITeamStore {
return;
}
this.loader = "mutation";
// get team member ids
const teamMemberIds = this.getTeamMemberIds(teamId);
// join team
await this.addTeamMembers(workspaceSlug, teamId, [currentUserId]);
await this.updateTeamMembers(workspaceSlug, teamId, uniq([...(teamMemberIds ?? []), currentUserId]), false);
this.loader = "loaded";
} catch (error) {
console.error("Failed to join team", error);