mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
Merge develop into revamp-estimates-ce
This commit is contained in:
parent
ea7a89ef87
commit
3ab9a233fe
@ -3,10 +3,11 @@ name: Build and Lint on Pull Request
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
types: ["opened", "synchronize"]
|
||||
types: ["opened", "synchronize", "ready_for_review"]
|
||||
|
||||
jobs:
|
||||
get-changed-files:
|
||||
if: github.event.pull_request.draft == false
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
apiserver_changed: ${{ steps.changed-files.outputs.apiserver_any_changed }}
|
||||
|
@ -7,12 +7,12 @@ import { useTheme } from "next-themes";
|
||||
import useSWR from "swr";
|
||||
import { Mails, KeyRound } from "lucide-react";
|
||||
import { TInstanceConfigurationKeys } from "@plane/types";
|
||||
import { Loader, setPromiseToast } from "@plane/ui";
|
||||
import { Loader, ToggleSwitch, setPromiseToast } from "@plane/ui";
|
||||
// components
|
||||
import { PageHeader } from "@/components/core";
|
||||
// hooks
|
||||
// helpers
|
||||
import { resolveGeneralTheme } from "@/helpers/common.helper";
|
||||
import { cn, resolveGeneralTheme } from "@/helpers/common.helper";
|
||||
import { useInstance } from "@/hooks/store";
|
||||
// images
|
||||
import githubLightModeImage from "@/public/logos/github-black.png";
|
||||
@ -45,6 +45,8 @@ const InstanceAuthenticationPage = observer(() => {
|
||||
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
||||
// theme
|
||||
const { resolvedTheme } = useTheme();
|
||||
// derived values
|
||||
const enableSignUpConfig = formattedConfig?.ENABLE_SIGNUP ?? "";
|
||||
|
||||
const updateConfig = async (key: TInstanceConfigurationKeys, value: string) => {
|
||||
setIsSubmitting(true);
|
||||
@ -129,7 +131,34 @@ const InstanceAuthenticationPage = observer(() => {
|
||||
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
|
||||
{formattedConfig ? (
|
||||
<div className="space-y-3">
|
||||
<div className="text-lg font-medium">Authentication modes</div>
|
||||
<div className="text-lg font-medium pb-1">Sign-up configuration</div>
|
||||
<div className={cn("w-full flex items-center gap-14 rounded")}>
|
||||
<div className="flex grow items-center gap-4">
|
||||
<div className="grow">
|
||||
<div className={cn("font-medium leading-5 text-custom-text-100 text-sm")}>
|
||||
Allow anyone to sign up without invite
|
||||
</div>
|
||||
<div className={cn("font-normal leading-5 text-custom-text-300 text-xs")}>
|
||||
Toggling this off will disable self sign ups.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`shrink-0 pr-4 ${isSubmitting && "opacity-70"}`}>
|
||||
<div className="flex items-center gap-4">
|
||||
<ToggleSwitch
|
||||
value={Boolean(parseInt(enableSignUpConfig))}
|
||||
onChange={() => {
|
||||
Boolean(parseInt(enableSignUpConfig)) === true
|
||||
? updateConfig("ENABLE_SIGNUP", "0")
|
||||
: updateConfig("ENABLE_SIGNUP", "1");
|
||||
}}
|
||||
size="sm"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-lg font-medium pt-6">Authentication modes</div>
|
||||
{authenticationMethodsCard.map((method) => (
|
||||
<AuthenticationMethodCard
|
||||
key={method.key}
|
||||
|
@ -45,7 +45,7 @@ export const HelpSection: FC = observer(() => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex w-full items-center justify-between gap-1 self-baseline border-t border-custom-border-200 bg-custom-sidebar-background-100 px-4 h-28",
|
||||
"flex w-full items-center justify-between gap-1 self-baseline border-t border-custom-border-200 bg-custom-sidebar-background-100 px-4 h-14 flex-shrink-0",
|
||||
{
|
||||
"flex-col h-auto py-1.5": isSidebarCollapsed,
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ from plane.db.models import (
|
||||
IssueProperty,
|
||||
Module,
|
||||
Project,
|
||||
ProjectDeployBoard,
|
||||
DeployBoard,
|
||||
ProjectMember,
|
||||
State,
|
||||
Workspace,
|
||||
@ -99,7 +99,7 @@ class ProjectAPIEndpoint(BaseAPIView):
|
||||
)
|
||||
.annotate(
|
||||
is_deployed=Exists(
|
||||
ProjectDeployBoard.objects.filter(
|
||||
DeployBoard.objects.filter(
|
||||
project_id=OuterRef("pk"),
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
)
|
||||
|
@ -30,7 +30,7 @@ from .project import (
|
||||
ProjectIdentifierSerializer,
|
||||
ProjectLiteSerializer,
|
||||
ProjectMemberLiteSerializer,
|
||||
ProjectDeployBoardSerializer,
|
||||
DeployBoardSerializer,
|
||||
ProjectMemberAdminSerializer,
|
||||
ProjectPublicMemberSerializer,
|
||||
ProjectMemberRoleSerializer,
|
||||
|
@ -13,7 +13,7 @@ from plane.db.models import (
|
||||
ProjectMember,
|
||||
ProjectMemberInvite,
|
||||
ProjectIdentifier,
|
||||
ProjectDeployBoard,
|
||||
DeployBoard,
|
||||
ProjectPublicMember,
|
||||
)
|
||||
|
||||
@ -206,14 +206,14 @@ class ProjectMemberLiteSerializer(BaseSerializer):
|
||||
read_only_fields = fields
|
||||
|
||||
|
||||
class ProjectDeployBoardSerializer(BaseSerializer):
|
||||
class DeployBoardSerializer(BaseSerializer):
|
||||
project_details = ProjectLiteSerializer(read_only=True, source="project")
|
||||
workspace_detail = WorkspaceLiteSerializer(
|
||||
read_only=True, source="workspace"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ProjectDeployBoard
|
||||
model = DeployBoard
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
|
@ -12,7 +12,7 @@ from plane.app.views import (
|
||||
ProjectFavoritesViewSet,
|
||||
UserProjectInvitationsViewset,
|
||||
ProjectPublicCoverImagesEndpoint,
|
||||
ProjectDeployBoardViewSet,
|
||||
DeployBoardViewSet,
|
||||
UserProjectRolesEndpoint,
|
||||
ProjectArchiveUnarchiveEndpoint,
|
||||
)
|
||||
@ -157,7 +157,7 @@ urlpatterns = [
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/project-deploy-boards/",
|
||||
ProjectDeployBoardViewSet.as_view(
|
||||
DeployBoardViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
@ -167,7 +167,7 @@ urlpatterns = [
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/project-deploy-boards/<uuid:pk>/",
|
||||
ProjectDeployBoardViewSet.as_view(
|
||||
DeployBoardViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"patch": "partial_update",
|
||||
|
@ -4,7 +4,7 @@ from .project.base import (
|
||||
ProjectUserViewsEndpoint,
|
||||
ProjectFavoritesViewSet,
|
||||
ProjectPublicCoverImagesEndpoint,
|
||||
ProjectDeployBoardViewSet,
|
||||
DeployBoardViewSet,
|
||||
ProjectArchiveUnarchiveEndpoint,
|
||||
)
|
||||
|
||||
|
@ -28,7 +28,7 @@ from plane.app.views.base import BaseViewSet, BaseAPIView
|
||||
from plane.app.serializers import (
|
||||
ProjectSerializer,
|
||||
ProjectListSerializer,
|
||||
ProjectDeployBoardSerializer,
|
||||
DeployBoardSerializer,
|
||||
)
|
||||
|
||||
from plane.app.permissions import (
|
||||
@ -46,7 +46,7 @@ from plane.db.models import (
|
||||
Module,
|
||||
Cycle,
|
||||
Inbox,
|
||||
ProjectDeployBoard,
|
||||
DeployBoard,
|
||||
IssueProperty,
|
||||
Issue,
|
||||
)
|
||||
@ -138,7 +138,7 @@ class ProjectViewSet(BaseViewSet):
|
||||
)
|
||||
.annotate(
|
||||
is_deployed=Exists(
|
||||
ProjectDeployBoard.objects.filter(
|
||||
DeployBoard.objects.filter(
|
||||
project_id=OuterRef("pk"),
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
)
|
||||
@ -639,12 +639,12 @@ class ProjectPublicCoverImagesEndpoint(BaseAPIView):
|
||||
return Response(files, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class ProjectDeployBoardViewSet(BaseViewSet):
|
||||
class DeployBoardViewSet(BaseViewSet):
|
||||
permission_classes = [
|
||||
ProjectMemberPermission,
|
||||
]
|
||||
serializer_class = ProjectDeployBoardSerializer
|
||||
model = ProjectDeployBoard
|
||||
serializer_class = DeployBoardSerializer
|
||||
model = DeployBoard
|
||||
|
||||
def get_queryset(self):
|
||||
return (
|
||||
@ -673,17 +673,17 @@ class ProjectDeployBoardViewSet(BaseViewSet):
|
||||
},
|
||||
)
|
||||
|
||||
project_deploy_board, _ = ProjectDeployBoard.objects.get_or_create(
|
||||
project_deploy_board, _ = DeployBoard.objects.get_or_create(
|
||||
anchor=f"{slug}/{project_id}",
|
||||
project_id=project_id,
|
||||
)
|
||||
project_deploy_board.comments = comments
|
||||
project_deploy_board.reactions = reactions
|
||||
project_deploy_board.inbox = inbox
|
||||
project_deploy_board.votes = votes
|
||||
project_deploy_board.views = views
|
||||
project_deploy_board.view_props = views
|
||||
project_deploy_board.is_votes_enabled = votes
|
||||
project_deploy_board.is_comments_enabled = comments
|
||||
project_deploy_board.is_reactions_enabled = reactions
|
||||
|
||||
project_deploy_board.save()
|
||||
|
||||
serializer = ProjectDeployBoardSerializer(project_deploy_board)
|
||||
serializer = DeployBoardSerializer(project_deploy_board)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
@ -4,6 +4,8 @@ import uuid
|
||||
|
||||
# Django imports
|
||||
from django.utils import timezone
|
||||
from django.core.validators import validate_email
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
# Third party imports
|
||||
from zxcvbn import zxcvbn
|
||||
@ -46,68 +48,71 @@ class Adapter:
|
||||
def authenticate(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def complete_login_or_signup(self):
|
||||
email = self.user_data.get("email")
|
||||
user = User.objects.filter(email=email).first()
|
||||
# Check if sign up case or login
|
||||
is_signup = bool(user)
|
||||
if not user:
|
||||
# New user
|
||||
(ENABLE_SIGNUP,) = get_configuration_value(
|
||||
[
|
||||
{
|
||||
"key": "ENABLE_SIGNUP",
|
||||
"default": os.environ.get("ENABLE_SIGNUP", "1"),
|
||||
},
|
||||
]
|
||||
)
|
||||
if (
|
||||
ENABLE_SIGNUP == "0"
|
||||
and not WorkspaceMemberInvite.objects.filter(
|
||||
email=email,
|
||||
).exists()
|
||||
):
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["SIGNUP_DISABLED"],
|
||||
error_message="SIGNUP_DISABLED",
|
||||
payload={"email": email},
|
||||
)
|
||||
user = User(email=email, username=uuid.uuid4().hex)
|
||||
|
||||
if self.user_data.get("user").get("is_password_autoset"):
|
||||
user.set_password(uuid.uuid4().hex)
|
||||
user.is_password_autoset = True
|
||||
user.is_email_verified = True
|
||||
else:
|
||||
# Validate password
|
||||
results = zxcvbn(self.code)
|
||||
if results["score"] < 3:
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[
|
||||
"INVALID_PASSWORD"
|
||||
],
|
||||
error_message="INVALID_PASSWORD",
|
||||
payload={"email": email},
|
||||
)
|
||||
|
||||
user.set_password(self.code)
|
||||
user.is_password_autoset = False
|
||||
|
||||
avatar = self.user_data.get("user", {}).get("avatar", "")
|
||||
first_name = self.user_data.get("user", {}).get("first_name", "")
|
||||
last_name = self.user_data.get("user", {}).get("last_name", "")
|
||||
user.avatar = avatar if avatar else ""
|
||||
user.first_name = first_name if first_name else ""
|
||||
user.last_name = last_name if last_name else ""
|
||||
user.save()
|
||||
Profile.objects.create(user=user)
|
||||
|
||||
if not user.is_active:
|
||||
def sanitize_email(self, email):
|
||||
# Check if email is present
|
||||
if not email:
|
||||
raise AuthenticationException(
|
||||
AUTHENTICATION_ERROR_CODES["USER_ACCOUNT_DEACTIVATED"],
|
||||
error_message="USER_ACCOUNT_DEACTIVATED",
|
||||
error_code=AUTHENTICATION_ERROR_CODES["INVALID_EMAIL"],
|
||||
error_message="INVALID_EMAIL",
|
||||
payload={"email": email},
|
||||
)
|
||||
|
||||
# Sanitize email
|
||||
email = str(email).lower().strip()
|
||||
|
||||
# validate email
|
||||
try:
|
||||
validate_email(email)
|
||||
except ValidationError:
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["INVALID_EMAIL"],
|
||||
error_message="INVALID_EMAIL",
|
||||
payload={"email": email},
|
||||
)
|
||||
# Return email
|
||||
return email
|
||||
|
||||
def validate_password(self, email):
|
||||
"""Validate password strength"""
|
||||
results = zxcvbn(self.code)
|
||||
if results["score"] < 3:
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["INVALID_PASSWORD"],
|
||||
error_message="INVALID_PASSWORD",
|
||||
payload={"email": email},
|
||||
)
|
||||
return
|
||||
|
||||
def __check_signup(self, email):
|
||||
"""Check if sign up is enabled or not and raise exception if not enabled"""
|
||||
|
||||
# Get configuration value
|
||||
(ENABLE_SIGNUP,) = get_configuration_value(
|
||||
[
|
||||
{
|
||||
"key": "ENABLE_SIGNUP",
|
||||
"default": os.environ.get("ENABLE_SIGNUP", "1"),
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
# Check if sign up is disabled and invite is present or not
|
||||
if (
|
||||
ENABLE_SIGNUP == "0"
|
||||
and not WorkspaceMemberInvite.objects.filter(
|
||||
email=email,
|
||||
).exists()
|
||||
):
|
||||
# Raise exception
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["SIGNUP_DISABLED"],
|
||||
error_message="SIGNUP_DISABLED",
|
||||
payload={"email": email},
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
def save_user_data(self, user):
|
||||
# Update user details
|
||||
user.last_login_medium = self.provider
|
||||
user.last_active = timezone.now()
|
||||
@ -116,7 +121,63 @@ class Adapter:
|
||||
user.last_login_uagent = self.request.META.get("HTTP_USER_AGENT")
|
||||
user.token_updated_at = timezone.now()
|
||||
user.save()
|
||||
return user
|
||||
|
||||
def complete_login_or_signup(self):
|
||||
# Get email
|
||||
email = self.user_data.get("email")
|
||||
|
||||
# Sanitize email
|
||||
email = self.sanitize_email(email)
|
||||
|
||||
# Check if the user is present
|
||||
user = User.objects.filter(email=email).first()
|
||||
# Check if sign up case or login
|
||||
is_signup = bool(user)
|
||||
# If user is not present, create a new user
|
||||
if not user:
|
||||
# New user
|
||||
self.__check_signup(email)
|
||||
|
||||
# Initialize user
|
||||
user = User(email=email, username=uuid.uuid4().hex)
|
||||
|
||||
# Check if password is autoset
|
||||
if self.user_data.get("user").get("is_password_autoset"):
|
||||
user.set_password(uuid.uuid4().hex)
|
||||
user.is_password_autoset = True
|
||||
user.is_email_verified = True
|
||||
|
||||
# Validate password
|
||||
else:
|
||||
# Validate password
|
||||
self.validate_password(email)
|
||||
# Set password
|
||||
user.set_password(self.code)
|
||||
user.is_password_autoset = False
|
||||
|
||||
# Set user details
|
||||
avatar = self.user_data.get("user", {}).get("avatar", "")
|
||||
first_name = self.user_data.get("user", {}).get("first_name", "")
|
||||
last_name = self.user_data.get("user", {}).get("last_name", "")
|
||||
user.avatar = avatar if avatar else ""
|
||||
user.first_name = first_name if first_name else ""
|
||||
user.last_name = last_name if last_name else ""
|
||||
user.save()
|
||||
|
||||
# Create profile
|
||||
Profile.objects.create(user=user)
|
||||
|
||||
if not user.is_active:
|
||||
raise AuthenticationException(
|
||||
AUTHENTICATION_ERROR_CODES["USER_ACCOUNT_DEACTIVATED"],
|
||||
error_message="USER_ACCOUNT_DEACTIVATED",
|
||||
)
|
||||
|
||||
# Save user data
|
||||
user = self.save_user_data(user=user)
|
||||
|
||||
# Call callback if present
|
||||
if self.callback:
|
||||
self.callback(
|
||||
user,
|
||||
@ -124,7 +185,9 @@ class Adapter:
|
||||
self.request,
|
||||
)
|
||||
|
||||
# Create or update account if token data is present
|
||||
if self.token_data:
|
||||
self.create_update_account(user=user)
|
||||
|
||||
# Return user
|
||||
return user
|
||||
|
@ -58,6 +58,8 @@ AUTHENTICATION_ERROR_CODES = {
|
||||
"ADMIN_USER_DEACTIVATED": 5190,
|
||||
# Rate limit
|
||||
"RATE_LIMIT_EXCEEDED": 5900,
|
||||
# Unknown
|
||||
"AUTHENTICATION_FAILED": 5999,
|
||||
}
|
||||
|
||||
|
||||
|
@ -81,11 +81,11 @@ class OauthAdapter(Adapter):
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except requests.RequestException:
|
||||
code = (
|
||||
"GOOGLE_OAUTH_PROVIDER_ERROR"
|
||||
if self.provider == "google"
|
||||
else "GITHUB_OAUTH_PROVIDER_ERROR"
|
||||
)
|
||||
if self.provider == "google":
|
||||
code = "GOOGLE_OAUTH_PROVIDER_ERROR"
|
||||
if self.provider == "github":
|
||||
code = "GITHUB_OAUTH_PROVIDER_ERROR"
|
||||
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[code],
|
||||
error_message=str(code),
|
||||
|
@ -4,6 +4,7 @@ from plane.db.models import (
|
||||
WorkspaceMember,
|
||||
WorkspaceMemberInvite,
|
||||
)
|
||||
from plane.utils.cache import invalidate_cache_directly
|
||||
|
||||
|
||||
def process_workspace_project_invitations(user):
|
||||
@ -26,6 +27,16 @@ def process_workspace_project_invitations(user):
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
|
||||
[
|
||||
invalidate_cache_directly(
|
||||
path=f"/api/workspaces/{str(workspace_member_invite.workspace.slug)}/members/",
|
||||
url_params=False,
|
||||
user=False,
|
||||
multiple=True,
|
||||
)
|
||||
for workspace_member_invite in workspace_member_invites
|
||||
]
|
||||
|
||||
# Check if user has any project invites
|
||||
project_member_invites = ProjectMemberInvite.objects.filter(
|
||||
email=user.email, accepted=True
|
||||
|
@ -28,6 +28,7 @@ from plane.db.models import (
|
||||
Project,
|
||||
State,
|
||||
User,
|
||||
EstimatePoint,
|
||||
)
|
||||
from plane.settings.redis import redis_instance
|
||||
from plane.utils.exception_logger import log_exception
|
||||
@ -448,21 +449,37 @@ def track_estimate_points(
|
||||
if current_instance.get("estimate_point") != requested_data.get(
|
||||
"estimate_point"
|
||||
):
|
||||
old_estimate = (
|
||||
EstimatePoint.objects.filter(
|
||||
pk=current_instance.get("estimate_point")
|
||||
).first()
|
||||
if current_instance.get("estimate_point") is not None
|
||||
else None
|
||||
)
|
||||
new_estimate = (
|
||||
EstimatePoint.objects.filter(
|
||||
pk=requested_data.get("estimate_point")
|
||||
).first()
|
||||
if requested_data.get("estimate_point") is not None
|
||||
else None
|
||||
)
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor_id=actor_id,
|
||||
verb="updated",
|
||||
old_value=(
|
||||
old_identifier=(
|
||||
current_instance.get("estimate_point")
|
||||
if current_instance.get("estimate_point") is not None
|
||||
else ""
|
||||
else None
|
||||
),
|
||||
new_value=(
|
||||
new_identifier=(
|
||||
requested_data.get("estimate_point")
|
||||
if requested_data.get("estimate_point") is not None
|
||||
else ""
|
||||
else None
|
||||
),
|
||||
old_value=old_estimate.value if old_estimate else None,
|
||||
new_value=new_estimate.value if new_estimate else None,
|
||||
field="estimate_point",
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
|
260
apiserver/plane/db/migrations/0067_issue_estimate.py
Normal file
260
apiserver/plane/db/migrations/0067_issue_estimate.py
Normal file
@ -0,0 +1,260 @@
|
||||
# # Generated by Django 4.2.7 on 2024-05-24 09:47
|
||||
# Python imports
|
||||
import uuid
|
||||
from uuid import uuid4
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import plane.db.models.deploy_board
|
||||
|
||||
|
||||
def issue_estimate_point(apps, schema_editor):
|
||||
Issue = apps.get_model("db", "Issue")
|
||||
Project = apps.get_model("db", "Project")
|
||||
EstimatePoint = apps.get_model("db", "EstimatePoint")
|
||||
IssueActivity = apps.get_model("db", "IssueActivity")
|
||||
updated_estimate_point = []
|
||||
updated_issue_activity = []
|
||||
|
||||
# loop through all the projects
|
||||
for project in Project.objects.filter(estimate__isnull=False):
|
||||
estimate_points = EstimatePoint.objects.filter(
|
||||
estimate=project.estimate, project=project
|
||||
)
|
||||
|
||||
for issue_activity in IssueActivity.objects.filter(
|
||||
field="estimate_point", project=project
|
||||
):
|
||||
if issue_activity.new_value:
|
||||
new_identifier = estimate_points.filter(
|
||||
key=issue_activity.new_value
|
||||
).first().id
|
||||
issue_activity.new_identifier = new_identifier
|
||||
new_value = estimate_points.filter(
|
||||
key=issue_activity.new_value
|
||||
).first().value
|
||||
issue_activity.new_value = new_value
|
||||
|
||||
if issue_activity.old_value:
|
||||
old_identifier = estimate_points.filter(
|
||||
key=issue_activity.old_value
|
||||
).first().id
|
||||
issue_activity.old_identifier = old_identifier
|
||||
old_value = estimate_points.filter(
|
||||
key=issue_activity.old_value
|
||||
).first().value
|
||||
issue_activity.old_value = old_value
|
||||
updated_issue_activity.append(issue_activity)
|
||||
|
||||
for issue in Issue.objects.filter(
|
||||
point__isnull=False, project=project
|
||||
):
|
||||
# get the estimate id for the corresponding estimate point in the issue
|
||||
estimate = estimate_points.filter(key=issue.point).first()
|
||||
issue.estimate_point = estimate
|
||||
updated_estimate_point.append(issue)
|
||||
|
||||
Issue.objects.bulk_update(
|
||||
updated_estimate_point, ["estimate_point"], batch_size=1000
|
||||
)
|
||||
IssueActivity.objects.bulk_update(
|
||||
updated_issue_activity,
|
||||
["new_value", "old_value", "new_identifier", "old_identifier"],
|
||||
batch_size=1000,
|
||||
)
|
||||
|
||||
|
||||
def last_used_estimate(apps, schema_editor):
|
||||
Project = apps.get_model("db", "Project")
|
||||
Estimate = apps.get_model("db", "Estimate")
|
||||
|
||||
# Get all estimate ids used in projects
|
||||
estimate_ids = Project.objects.filter(estimate__isnull=False).values_list(
|
||||
"estimate", flat=True
|
||||
)
|
||||
|
||||
# Update all matching estimates
|
||||
Estimate.objects.filter(id__in=estimate_ids).update(last_used=True)
|
||||
|
||||
|
||||
def populate_deploy_board(apps, schema_editor):
|
||||
DeployBoard = apps.get_model("db", "DeployBoard")
|
||||
ProjectDeployBoard = apps.get_model("db", "ProjectDeployBoard")
|
||||
|
||||
DeployBoard.objects.bulk_create(
|
||||
[
|
||||
DeployBoard(
|
||||
entity_identifier=deploy_board.project_id,
|
||||
project_id=deploy_board.project_id,
|
||||
entity_name="project",
|
||||
anchor=uuid4().hex,
|
||||
is_comments_enabled=deploy_board.comments,
|
||||
is_reactions_enabled=deploy_board.reactions,
|
||||
inbox=deploy_board.inbox,
|
||||
is_votes_enabled=deploy_board.votes,
|
||||
view_props=deploy_board.views,
|
||||
workspace_id=deploy_board.workspace_id,
|
||||
created_at=deploy_board.created_at,
|
||||
updated_at=deploy_board.updated_at,
|
||||
created_by_id=deploy_board.created_by_id,
|
||||
updated_by_id=deploy_board.updated_by_id,
|
||||
)
|
||||
for deploy_board in ProjectDeployBoard.objects.all()
|
||||
],
|
||||
batch_size=100,
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("db", "0066_account_id_token_cycle_logo_props_module_logo_props"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="DeployBoard",
|
||||
fields=[
|
||||
(
|
||||
"created_at",
|
||||
models.DateTimeField(
|
||||
auto_now_add=True, verbose_name="Created At"
|
||||
),
|
||||
),
|
||||
(
|
||||
"updated_at",
|
||||
models.DateTimeField(
|
||||
auto_now=True, verbose_name="Last Modified At"
|
||||
),
|
||||
),
|
||||
(
|
||||
"id",
|
||||
models.UUIDField(
|
||||
db_index=True,
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
unique=True,
|
||||
),
|
||||
),
|
||||
("entity_identifier", models.UUIDField(null=True)),
|
||||
(
|
||||
"entity_name",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("project", "Project"),
|
||||
("issue", "Issue"),
|
||||
("module", "Module"),
|
||||
("cycle", "Task"),
|
||||
("page", "Page"),
|
||||
("view", "View"),
|
||||
],
|
||||
max_length=30,
|
||||
),
|
||||
),
|
||||
(
|
||||
"anchor",
|
||||
models.CharField(
|
||||
db_index=True,
|
||||
default=plane.db.models.deploy_board.get_anchor,
|
||||
max_length=255,
|
||||
unique=True,
|
||||
),
|
||||
),
|
||||
("is_comments_enabled", models.BooleanField(default=False)),
|
||||
("is_reactions_enabled", models.BooleanField(default=False)),
|
||||
("is_votes_enabled", models.BooleanField(default=False)),
|
||||
("view_props", models.JSONField(default=dict)),
|
||||
(
|
||||
"created_by",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="%(class)s_created_by",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
verbose_name="Created By",
|
||||
),
|
||||
),
|
||||
(
|
||||
"inbox",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="board_inbox",
|
||||
to="db.inbox",
|
||||
),
|
||||
),
|
||||
(
|
||||
"project",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="project_%(class)s",
|
||||
to="db.project",
|
||||
),
|
||||
),
|
||||
(
|
||||
"updated_by",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="%(class)s_updated_by",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
verbose_name="Last Modified By",
|
||||
),
|
||||
),
|
||||
(
|
||||
"workspace",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="workspace_%(class)s",
|
||||
to="db.workspace",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Deploy Board",
|
||||
"verbose_name_plural": "Deploy Boards",
|
||||
"db_table": "deploy_boards",
|
||||
"ordering": ("-created_at",),
|
||||
"unique_together": {("entity_name", "entity_identifier")},
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="estimate",
|
||||
name="last_used",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
# Rename the existing field
|
||||
migrations.RenameField(
|
||||
model_name="issue",
|
||||
old_name="estimate_point",
|
||||
new_name="point",
|
||||
),
|
||||
# Add a new field with the original name as a foreign key
|
||||
migrations.AddField(
|
||||
model_name="issue",
|
||||
name="estimate_point",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="issue_estimates",
|
||||
to="db.EstimatePoint",
|
||||
blank=True,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="estimate",
|
||||
name="type",
|
||||
field=models.CharField(default="categories", max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="estimatepoint",
|
||||
name="value",
|
||||
field=models.CharField(max_length=255),
|
||||
),
|
||||
migrations.RunPython(issue_estimate_point),
|
||||
migrations.RunPython(last_used_estimate),
|
||||
migrations.RunPython(populate_deploy_board),
|
||||
]
|
@ -4,6 +4,7 @@ from .asset import FileAsset
|
||||
from .base import BaseModel
|
||||
from .cycle import Cycle, CycleFavorite, CycleIssue, CycleUserProperties
|
||||
from .dashboard import Dashboard, DashboardWidget, Widget
|
||||
from .deploy_board import DeployBoard
|
||||
from .estimate import Estimate, EstimatePoint
|
||||
from .exporter import ExporterHistory
|
||||
from .importer import Importer
|
||||
@ -53,7 +54,6 @@ from .page import Page, PageFavorite, PageLabel, PageLog
|
||||
from .project import (
|
||||
Project,
|
||||
ProjectBaseModel,
|
||||
ProjectDeployBoard,
|
||||
ProjectFavorite,
|
||||
ProjectIdentifier,
|
||||
ProjectMember,
|
||||
|
@ -12,6 +12,7 @@ from .base import BaseModel
|
||||
|
||||
|
||||
def get_upload_path(instance, filename):
|
||||
filename = filename[:50]
|
||||
if instance.workspace_id is not None:
|
||||
return f"{instance.workspace.id}/{uuid4().hex}-{filename}"
|
||||
return f"user-{uuid4().hex}-{filename}"
|
||||
|
53
apiserver/plane/db/models/deploy_board.py
Normal file
53
apiserver/plane/db/models/deploy_board.py
Normal file
@ -0,0 +1,53 @@
|
||||
# Python imports
|
||||
from uuid import uuid4
|
||||
|
||||
# Django imports
|
||||
from django.db import models
|
||||
|
||||
# Module imports
|
||||
from .workspace import WorkspaceBaseModel
|
||||
|
||||
|
||||
def get_anchor():
|
||||
return uuid4().hex
|
||||
|
||||
|
||||
class DeployBoard(WorkspaceBaseModel):
|
||||
TYPE_CHOICES = (
|
||||
("project", "Project"),
|
||||
("issue", "Issue"),
|
||||
("module", "Module"),
|
||||
("cycle", "Task"),
|
||||
("page", "Page"),
|
||||
("view", "View"),
|
||||
)
|
||||
|
||||
entity_identifier = models.UUIDField(null=True)
|
||||
entity_name = models.CharField(
|
||||
max_length=30,
|
||||
choices=TYPE_CHOICES,
|
||||
)
|
||||
anchor = models.CharField(
|
||||
max_length=255, default=get_anchor, unique=True, db_index=True
|
||||
)
|
||||
is_comments_enabled = models.BooleanField(default=False)
|
||||
is_reactions_enabled = models.BooleanField(default=False)
|
||||
inbox = models.ForeignKey(
|
||||
"db.Inbox",
|
||||
related_name="board_inbox",
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
)
|
||||
is_votes_enabled = models.BooleanField(default=False)
|
||||
view_props = models.JSONField(default=dict)
|
||||
|
||||
def __str__(self):
|
||||
"""Return name of the deploy board"""
|
||||
return f"{self.entity_identifier} <{self.entity_name}>"
|
||||
|
||||
class Meta:
|
||||
unique_together = ["entity_name", "entity_identifier"]
|
||||
verbose_name = "Deploy Board"
|
||||
verbose_name_plural = "Deploy Boards"
|
||||
db_table = "deploy_boards"
|
||||
ordering = ("-created_at",)
|
@ -11,7 +11,7 @@ class Estimate(ProjectBaseModel):
|
||||
description = models.TextField(
|
||||
verbose_name="Estimate Description", blank=True
|
||||
)
|
||||
type = models.CharField(max_length=255, default="Categories")
|
||||
type = models.CharField(max_length=255, default="categories")
|
||||
last_used = models.BooleanField(default=False)
|
||||
|
||||
def __str__(self):
|
||||
@ -36,7 +36,7 @@ class EstimatePoint(ProjectBaseModel):
|
||||
default=0, validators=[MinValueValidator(0), MaxValueValidator(12)]
|
||||
)
|
||||
description = models.TextField(blank=True)
|
||||
value = models.CharField(max_length=20)
|
||||
value = models.CharField(max_length=255)
|
||||
|
||||
def __str__(self):
|
||||
"""Return name of the estimate"""
|
||||
|
@ -127,7 +127,7 @@ class Issue(ProjectBaseModel):
|
||||
estimate_point = models.ForeignKey(
|
||||
"db.EstimatePoint",
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="issue_estimate",
|
||||
related_name="issue_estimates",
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
@ -260,6 +260,8 @@ def get_default_views():
|
||||
}
|
||||
|
||||
|
||||
# DEPRECATED TODO:
|
||||
# used to get the old anchors for the project deploy boards
|
||||
class ProjectDeployBoard(ProjectBaseModel):
|
||||
anchor = models.CharField(
|
||||
max_length=255, default=get_anchor, unique=True, db_index=True
|
||||
|
@ -18,7 +18,7 @@ from plane.db.models import (
|
||||
State,
|
||||
IssueLink,
|
||||
IssueAttachment,
|
||||
ProjectDeployBoard,
|
||||
DeployBoard,
|
||||
)
|
||||
from plane.app.serializers import (
|
||||
IssueSerializer,
|
||||
@ -39,7 +39,7 @@ class InboxIssuePublicViewSet(BaseViewSet):
|
||||
]
|
||||
|
||||
def get_queryset(self):
|
||||
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||
project_deploy_board = DeployBoard.objects.get(
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
)
|
||||
@ -59,7 +59,7 @@ class InboxIssuePublicViewSet(BaseViewSet):
|
||||
return InboxIssue.objects.none()
|
||||
|
||||
def list(self, request, slug, project_id, inbox_id):
|
||||
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||
project_deploy_board = DeployBoard.objects.get(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
if project_deploy_board.inbox is None:
|
||||
@ -118,7 +118,7 @@ class InboxIssuePublicViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
def create(self, request, slug, project_id, inbox_id):
|
||||
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||
project_deploy_board = DeployBoard.objects.get(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
if project_deploy_board.inbox is None:
|
||||
@ -189,7 +189,7 @@ class InboxIssuePublicViewSet(BaseViewSet):
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
def partial_update(self, request, slug, project_id, inbox_id, pk):
|
||||
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||
project_deploy_board = DeployBoard.objects.get(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
if project_deploy_board.inbox is None:
|
||||
@ -256,7 +256,7 @@ class InboxIssuePublicViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
def retrieve(self, request, slug, project_id, inbox_id, pk):
|
||||
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||
project_deploy_board = DeployBoard.objects.get(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
if project_deploy_board.inbox is None:
|
||||
@ -280,7 +280,7 @@ class InboxIssuePublicViewSet(BaseViewSet):
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
def destroy(self, request, slug, project_id, inbox_id, pk):
|
||||
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||
project_deploy_board = DeployBoard.objects.get(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
if project_deploy_board.inbox is None:
|
||||
|
@ -44,7 +44,7 @@ from plane.db.models import (
|
||||
ProjectMember,
|
||||
IssueReaction,
|
||||
CommentReaction,
|
||||
ProjectDeployBoard,
|
||||
DeployBoard,
|
||||
IssueVote,
|
||||
ProjectPublicMember,
|
||||
)
|
||||
@ -76,7 +76,7 @@ class IssueCommentPublicViewSet(BaseViewSet):
|
||||
|
||||
def get_queryset(self):
|
||||
try:
|
||||
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||
project_deploy_board = DeployBoard.objects.get(
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
)
|
||||
@ -103,11 +103,11 @@ class IssueCommentPublicViewSet(BaseViewSet):
|
||||
.distinct()
|
||||
).order_by("created_at")
|
||||
return IssueComment.objects.none()
|
||||
except ProjectDeployBoard.DoesNotExist:
|
||||
except DeployBoard.DoesNotExist:
|
||||
return IssueComment.objects.none()
|
||||
|
||||
def create(self, request, slug, project_id, issue_id):
|
||||
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||
project_deploy_board = DeployBoard.objects.get(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
|
||||
@ -151,7 +151,7 @@ class IssueCommentPublicViewSet(BaseViewSet):
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def partial_update(self, request, slug, project_id, issue_id, pk):
|
||||
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||
project_deploy_board = DeployBoard.objects.get(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
|
||||
@ -184,7 +184,7 @@ class IssueCommentPublicViewSet(BaseViewSet):
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def destroy(self, request, slug, project_id, issue_id, pk):
|
||||
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||
project_deploy_board = DeployBoard.objects.get(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
|
||||
@ -221,7 +221,7 @@ class IssueReactionPublicViewSet(BaseViewSet):
|
||||
|
||||
def get_queryset(self):
|
||||
try:
|
||||
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||
project_deploy_board = DeployBoard.objects.get(
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
)
|
||||
@ -236,11 +236,11 @@ class IssueReactionPublicViewSet(BaseViewSet):
|
||||
.distinct()
|
||||
)
|
||||
return IssueReaction.objects.none()
|
||||
except ProjectDeployBoard.DoesNotExist:
|
||||
except DeployBoard.DoesNotExist:
|
||||
return IssueReaction.objects.none()
|
||||
|
||||
def create(self, request, slug, project_id, issue_id):
|
||||
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||
project_deploy_board = DeployBoard.objects.get(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
|
||||
@ -280,7 +280,7 @@ class IssueReactionPublicViewSet(BaseViewSet):
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def destroy(self, request, slug, project_id, issue_id, reaction_code):
|
||||
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||
project_deploy_board = DeployBoard.objects.get(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
|
||||
@ -319,7 +319,7 @@ class CommentReactionPublicViewSet(BaseViewSet):
|
||||
|
||||
def get_queryset(self):
|
||||
try:
|
||||
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||
project_deploy_board = DeployBoard.objects.get(
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
)
|
||||
@ -334,11 +334,11 @@ class CommentReactionPublicViewSet(BaseViewSet):
|
||||
.distinct()
|
||||
)
|
||||
return CommentReaction.objects.none()
|
||||
except ProjectDeployBoard.DoesNotExist:
|
||||
except DeployBoard.DoesNotExist:
|
||||
return CommentReaction.objects.none()
|
||||
|
||||
def create(self, request, slug, project_id, comment_id):
|
||||
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||
project_deploy_board = DeployBoard.objects.get(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
|
||||
@ -380,7 +380,7 @@ class CommentReactionPublicViewSet(BaseViewSet):
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def destroy(self, request, slug, project_id, comment_id, reaction_code):
|
||||
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||
project_deploy_board = DeployBoard.objects.get(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
if not project_deploy_board.reactions:
|
||||
@ -421,7 +421,7 @@ class IssueVotePublicViewSet(BaseViewSet):
|
||||
|
||||
def get_queryset(self):
|
||||
try:
|
||||
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||
project_deploy_board = DeployBoard.objects.get(
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
)
|
||||
@ -434,7 +434,7 @@ class IssueVotePublicViewSet(BaseViewSet):
|
||||
.filter(project_id=self.kwargs.get("project_id"))
|
||||
)
|
||||
return IssueVote.objects.none()
|
||||
except ProjectDeployBoard.DoesNotExist:
|
||||
except DeployBoard.DoesNotExist:
|
||||
return IssueVote.objects.none()
|
||||
|
||||
def create(self, request, slug, project_id, issue_id):
|
||||
@ -513,7 +513,7 @@ class ProjectIssuesPublicEndpoint(BaseAPIView):
|
||||
]
|
||||
|
||||
def get(self, request, slug, project_id):
|
||||
if not ProjectDeployBoard.objects.filter(
|
||||
if not DeployBoard.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
).exists():
|
||||
return Response(
|
||||
|
@ -11,10 +11,10 @@ from rest_framework.permissions import AllowAny
|
||||
|
||||
# Module imports
|
||||
from .base import BaseAPIView
|
||||
from plane.app.serializers import ProjectDeployBoardSerializer
|
||||
from plane.app.serializers import DeployBoardSerializer
|
||||
from plane.db.models import (
|
||||
Project,
|
||||
ProjectDeployBoard,
|
||||
DeployBoard,
|
||||
)
|
||||
|
||||
|
||||
@ -24,10 +24,10 @@ class ProjectDeployBoardPublicSettingsEndpoint(BaseAPIView):
|
||||
]
|
||||
|
||||
def get(self, request, slug, project_id):
|
||||
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||
project_deploy_board = DeployBoard.objects.get(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
serializer = ProjectDeployBoardSerializer(project_deploy_board)
|
||||
serializer = DeployBoardSerializer(project_deploy_board)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
@ -41,7 +41,7 @@ class WorkspaceProjectDeployBoardEndpoint(BaseAPIView):
|
||||
Project.objects.filter(workspace__slug=slug)
|
||||
.annotate(
|
||||
is_public=Exists(
|
||||
ProjectDeployBoard.objects.filter(
|
||||
DeployBoard.objects.filter(
|
||||
workspace__slug=slug, project_id=OuterRef("pk")
|
||||
)
|
||||
)
|
||||
|
@ -66,7 +66,7 @@ def invalidate_cache_directly(
|
||||
custom_path = path if path is not None else request.get_full_path()
|
||||
auth_header = (
|
||||
None
|
||||
if request.user.is_anonymous
|
||||
if request and request.user.is_anonymous
|
||||
else str(request.user.id) if user else None
|
||||
)
|
||||
key = generate_cache_key(custom_path, auth_header)
|
||||
|
@ -112,7 +112,7 @@ export const useEditor = ({
|
||||
if (value === null || value === undefined) return;
|
||||
if (editor && !editor.isDestroyed && !editor.storage.image.uploadInProgress) {
|
||||
try {
|
||||
editor.commands.setContent(value);
|
||||
editor.commands.setContent(value, false, { preserveWhitespace: "full" });
|
||||
const currentSavedSelection = savedSelectionRef.current;
|
||||
if (currentSavedSelection) {
|
||||
const docLength = editor.state.doc.content.size;
|
||||
|
@ -50,9 +50,25 @@ export async function startImageUpload(
|
||||
};
|
||||
|
||||
try {
|
||||
const fileNameTrimmed = trimFileName(file.name);
|
||||
const fileWithTrimmedName = new File([file], fileNameTrimmed, { type: file.type });
|
||||
|
||||
const resolvedPos = view.state.doc.resolve(pos ?? 0);
|
||||
const nodeBefore = resolvedPos.nodeBefore;
|
||||
|
||||
// if the image is at the start of the line i.e. when nodeBefore is null
|
||||
if (nodeBefore === null) {
|
||||
if (pos) {
|
||||
// so that the image is not inserted at the next line, else incase the
|
||||
// image is inserted at any line where there's some content, the
|
||||
// position is kept as it is to be inserted at the next line
|
||||
pos -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
view.focus();
|
||||
|
||||
const src = await uploadAndValidateImage(file, uploadFile);
|
||||
const src = await uploadAndValidateImage(fileWithTrimmedName, uploadFile);
|
||||
|
||||
if (src == null) {
|
||||
throw new Error("Resolved image URL is undefined.");
|
||||
@ -112,3 +128,14 @@ async function uploadAndValidateImage(file: File, uploadFile: UploadImage): Prom
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function trimFileName(fileName: string, maxLength = 100) {
|
||||
if (fileName.length > maxLength) {
|
||||
const extension = fileName.split(".").pop();
|
||||
const nameWithoutExtension = fileName.slice(0, -(extension?.length ?? 0 + 1));
|
||||
const allowedNameLength = maxLength - (extension?.length ?? 0) - 1; // -1 for the dot
|
||||
return `${nameWithoutExtension.slice(0, allowedNameLength)}.${extension}`;
|
||||
}
|
||||
|
||||
return fileName;
|
||||
}
|
||||
|
@ -20,12 +20,15 @@
|
||||
"postcss": "postcss styles/globals.css -o styles/output.css --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.1.10",
|
||||
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3",
|
||||
"@blueprintjs/core": "^4.16.3",
|
||||
"@blueprintjs/popover2": "^1.13.3",
|
||||
"@headlessui/react": "^2.0.3",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"clsx": "^2.0.0",
|
||||
"emoji-picker-react": "^4.5.16",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.379.0",
|
||||
"react-color": "^2.19.3",
|
||||
"react-dom": "^18.2.0",
|
||||
|
@ -7,10 +7,11 @@ export type TControlLink = React.AnchorHTMLAttributes<HTMLAnchorElement> & {
|
||||
target?: string;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
draggable?: boolean;
|
||||
};
|
||||
|
||||
export const ControlLink = React.forwardRef<HTMLAnchorElement, TControlLink>((props, ref) => {
|
||||
const { href, onClick, children, target = "_self", disabled = false, className, ...rest } = props;
|
||||
const { href, onClick, children, target = "_self", disabled = false, className, draggable = false, ...rest } = props;
|
||||
const LEFT_CLICK_EVENT_CODE = 0;
|
||||
|
||||
const handleOnClick = (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
|
||||
@ -33,7 +34,15 @@ export const ControlLink = React.forwardRef<HTMLAnchorElement, TControlLink>((pr
|
||||
if (disabled) return <>{children}</>;
|
||||
|
||||
return (
|
||||
<a href={href} target={target} onClick={handleOnClick} {...rest} ref={ref} className={className}>
|
||||
<a
|
||||
href={href}
|
||||
target={target}
|
||||
onClick={handleOnClick}
|
||||
{...rest}
|
||||
ref={ref}
|
||||
className={className}
|
||||
draggable={draggable}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
|
@ -37,7 +37,7 @@ const Draggable = ({ children, data, className }: Props) => {
|
||||
onDrop: () => {
|
||||
setIsDraggedOver(false);
|
||||
},
|
||||
canDrop: ({ source }) => !isEqual(source.data, data),
|
||||
canDrop: ({ source }) => !isEqual(source.data, data) && source.data.__uuid__ === data.__uuid__,
|
||||
getData: ({ input, element }) =>
|
||||
attachClosestEdge(data, {
|
||||
input,
|
||||
@ -53,7 +53,7 @@ const Draggable = ({ children, data, className }: Props) => {
|
||||
<div ref={ref} className={cn(dragging && "opacity-25", className)}>
|
||||
{<DropIndicator isVisible={isDraggedOver && closestEdge === "top"} />}
|
||||
{children}
|
||||
{<DropIndicator isVisible={isDraggedOver && closestEdge === "bottom"} classNames="absolute w-full" />}
|
||||
{<DropIndicator isVisible={isDraggedOver && closestEdge === "bottom"} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { Fragment, useEffect } from "react";
|
||||
import React, { Fragment, useEffect, useMemo } from "react";
|
||||
import { monitorForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
|
||||
import { Draggable } from "./draggable";
|
||||
|
||||
@ -8,6 +8,7 @@ type Props<T> = {
|
||||
onChange: (data: T[]) => void;
|
||||
keyExtractor: (item: T, index: number) => string;
|
||||
containerClassName?: string;
|
||||
id?: string;
|
||||
};
|
||||
|
||||
const moveItem = <T,>(
|
||||
@ -43,7 +44,7 @@ const moveItem = <T,>(
|
||||
return newData;
|
||||
};
|
||||
|
||||
export const Sortable = <T,>({ data, render, onChange, keyExtractor, containerClassName }: Props<T>) => {
|
||||
export const Sortable = <T,>({ data, render, onChange, keyExtractor, containerClassName, id }: Props<T>) => {
|
||||
useEffect(() => {
|
||||
const unsubscribe = monitorForElements({
|
||||
onDrop({ source, location }) {
|
||||
@ -57,11 +58,16 @@ export const Sortable = <T,>({ data, render, onChange, keyExtractor, containerCl
|
||||
return () => {
|
||||
if (unsubscribe) unsubscribe();
|
||||
};
|
||||
}, [data, onChange]);
|
||||
}, [data, keyExtractor, onChange]);
|
||||
|
||||
const enhancedData = useMemo(() => {
|
||||
const uuid = id ? id : Math.random().toString(36).substring(7);
|
||||
return data.map((item) => ({ ...item, __uuid__: uuid }));
|
||||
}, [data, id]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{data.map((item, index) => (
|
||||
{enhancedData.map((item, index) => (
|
||||
<Draggable key={keyExtractor(item, index)} data={item} className={containerClassName}>
|
||||
<Fragment>{render(item, index)} </Fragment>
|
||||
</Draggable>
|
||||
|
@ -24,10 +24,7 @@ import { Tooltip, BlockedIcon, BlockerIcon, RelatedIcon, LayersIcon, DiceIcon }
|
||||
// helpers
|
||||
import { renderFormattedDate } from "@/helpers/date-time.helper";
|
||||
import { capitalizeFirstLetter } from "@/helpers/string.helper";
|
||||
import {
|
||||
// useEstimate,
|
||||
useLabel,
|
||||
} from "@/hooks/store";
|
||||
import { useLabel } from "@/hooks/store";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// types
|
||||
|
||||
@ -100,22 +97,6 @@ const LabelPill = observer(({ labelId, workspaceSlug }: { labelId: string; works
|
||||
);
|
||||
});
|
||||
|
||||
// const EstimatePoint = observer((props: { point: string }) => {
|
||||
// const { point } = props;
|
||||
// const { areEstimatesEnabledForCurrentProject, getEstimatePointValue } = useEstimate();
|
||||
// const currentPoint = Number(point) + 1;
|
||||
|
||||
// const estimateValue = getEstimatePointValue(Number(point), null);
|
||||
|
||||
// return (
|
||||
// <span className="font-medium text-custom-text-100 whitespace-nowrap">
|
||||
// {areEstimatesEnabledForCurrentProject
|
||||
// ? estimateValue
|
||||
// : `${currentPoint} ${currentPoint > 1 ? "points" : "point"}`}
|
||||
// </span>
|
||||
// );
|
||||
// });
|
||||
|
||||
const inboxActivityMessage = {
|
||||
declined: {
|
||||
showIssue: "declined issue",
|
||||
@ -270,8 +251,7 @@ const activityDetails: {
|
||||
else
|
||||
return (
|
||||
<>
|
||||
set the estimate point to
|
||||
{/* <EstimatePoint point={activity.new_value} /> */}
|
||||
set the estimate point to {activity.new_value}
|
||||
{showIssue && (
|
||||
<>
|
||||
{" "}
|
||||
|
@ -162,7 +162,7 @@ export const GanttChartMainContent: React.FC<Props> = observer((props) => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<IssueBulkOperationsRoot />
|
||||
<IssueBulkOperationsRoot selectionHelpers={helpers} />
|
||||
</>
|
||||
)}
|
||||
</MultipleSelectGroup>
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { FC, useState } from "react";
|
||||
import { FC, useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import useSWR from "swr";
|
||||
import { Inbox, PanelLeft } from "lucide-react";
|
||||
// components
|
||||
import { EmptyState } from "@/components/empty-state";
|
||||
@ -10,6 +9,7 @@ import { InboxLayoutLoader } from "@/components/ui";
|
||||
import { EmptyStateType } from "@/constants/empty-state";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { EInboxIssueCurrentTab } from "@/helpers/inbox.helper";
|
||||
// hooks
|
||||
import { useProjectInbox } from "@/hooks/store";
|
||||
|
||||
@ -18,25 +18,25 @@ type TInboxIssueRoot = {
|
||||
projectId: string;
|
||||
inboxIssueId: string | undefined;
|
||||
inboxAccessible: boolean;
|
||||
navigationTab?: EInboxIssueCurrentTab | undefined;
|
||||
};
|
||||
|
||||
export const InboxIssueRoot: FC<TInboxIssueRoot> = observer((props) => {
|
||||
const { workspaceSlug, projectId, inboxIssueId, inboxAccessible } = props;
|
||||
const { workspaceSlug, projectId, inboxIssueId, inboxAccessible, navigationTab } = props;
|
||||
// states
|
||||
const [isMobileSidebar, setIsMobileSidebar] = useState(true);
|
||||
// hooks
|
||||
const { loader, error, fetchInboxIssues } = useProjectInbox();
|
||||
const { loader, error, currentTab, handleCurrentTab, fetchInboxIssues } = useProjectInbox();
|
||||
|
||||
useSWR(
|
||||
inboxAccessible && workspaceSlug && projectId ? `PROJECT_INBOX_ISSUES_${workspaceSlug}_${projectId}` : null,
|
||||
async () => {
|
||||
inboxAccessible &&
|
||||
workspaceSlug &&
|
||||
projectId &&
|
||||
(await fetchInboxIssues(workspaceSlug.toString(), projectId.toString()));
|
||||
},
|
||||
{ revalidateOnFocus: false, revalidateIfStale: false }
|
||||
);
|
||||
useEffect(() => {
|
||||
if (!inboxAccessible || !workspaceSlug || !projectId) return;
|
||||
if (navigationTab && navigationTab !== currentTab) {
|
||||
handleCurrentTab(navigationTab);
|
||||
} else {
|
||||
fetchInboxIssues(workspaceSlug.toString(), projectId.toString());
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [inboxAccessible, workspaceSlug, projectId]);
|
||||
|
||||
// loader
|
||||
if (loader === "init-loading")
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { FC, useCallback, useRef } from "react";
|
||||
import { FC, useCallback, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { TInboxIssueCurrentTab } from "@plane/types";
|
||||
@ -37,7 +37,7 @@ export const InboxSidebar: FC<IInboxSidebarProps> = observer((props) => {
|
||||
const { workspaceSlug, projectId, setIsMobileSidebar } = props;
|
||||
// ref
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const elementRef = useRef<HTMLDivElement>(null);
|
||||
const [elementRef, setElementRef] = useState<HTMLDivElement | null>(null);
|
||||
// store
|
||||
const { currentProjectDetails } = useProject();
|
||||
const {
|
||||
@ -72,8 +72,10 @@ export const InboxSidebar: FC<IInboxSidebarProps> = observer((props) => {
|
||||
currentTab === option?.key ? `text-custom-primary-100` : `hover:text-custom-text-200`
|
||||
)}
|
||||
onClick={() => {
|
||||
if (currentTab != option?.key) handleCurrentTab(option?.key);
|
||||
router.push(`/${workspaceSlug}/projects/${projectId}/inbox?currentTab=${option?.key}`);
|
||||
if (currentTab != option?.key) {
|
||||
handleCurrentTab(option?.key);
|
||||
router.push(`/${workspaceSlug}/projects/${projectId}/inbox?currentTab=${option?.key}`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div>{option?.label}</div>
|
||||
@ -126,14 +128,14 @@ export const InboxSidebar: FC<IInboxSidebarProps> = observer((props) => {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{inboxIssuePaginationInfo?.next_page_results && (
|
||||
<div ref={elementRef}>
|
||||
<div ref={setElementRef}>
|
||||
{inboxIssuePaginationInfo?.next_page_results && (
|
||||
<Loader className="mx-auto w-full space-y-4 py-4 px-2">
|
||||
<Loader.Item height="64px" width="w-100" />
|
||||
<Loader.Item height="64px" width="w-100" />
|
||||
</Loader>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@ -3,9 +3,11 @@ import { observer } from "mobx-react";
|
||||
import { BulkOperationsUpgradeBanner } from "@/components/issues";
|
||||
// hooks
|
||||
import { useMultipleSelectStore } from "@/hooks/store";
|
||||
import { TSelectionHelper } from "@/hooks/use-multiple-select";
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
selectionHelpers: TSelectionHelper;
|
||||
};
|
||||
|
||||
export const IssueBulkOperationsRoot: React.FC<Props> = observer((props) => {
|
||||
|
@ -2,10 +2,7 @@ import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Triangle } from "lucide-react";
|
||||
// hooks
|
||||
import {
|
||||
// useEstimate,
|
||||
useIssueDetail,
|
||||
} from "@/hooks/store";
|
||||
import { useIssueDetail } from "@/hooks/store";
|
||||
// components
|
||||
import { IssueActivityBlockComponent, IssueLink } from "./";
|
||||
|
||||
@ -17,17 +14,11 @@ export const IssueEstimateActivity: FC<TIssueEstimateActivity> = observer((props
|
||||
const {
|
||||
activity: { getActivityById },
|
||||
} = useIssueDetail();
|
||||
// const { areEstimatesEnabledForCurrentProject, getEstimatePointValue } = useEstimate();
|
||||
const areEstimatesEnabledForCurrentProject = false;
|
||||
|
||||
const activity = getActivityById(activityId);
|
||||
|
||||
if (!activity) return <></>;
|
||||
|
||||
// const estimateValue = getEstimatePointValue(Number(activity.new_value), null);
|
||||
const estimateValue = "None";
|
||||
const currentPoint = Number(activity.new_value) + 1;
|
||||
|
||||
return (
|
||||
<IssueActivityBlockComponent
|
||||
icon={<Triangle size={14} color="#6b7280" aria-hidden="true" />}
|
||||
@ -36,15 +27,7 @@ export const IssueEstimateActivity: FC<TIssueEstimateActivity> = observer((props
|
||||
>
|
||||
<>
|
||||
{activity.new_value ? `set the estimate point to ` : `removed the estimate point `}
|
||||
{activity.new_value && (
|
||||
<>
|
||||
<span className="font-medium text-custom-text-100">
|
||||
{areEstimatesEnabledForCurrentProject
|
||||
? estimateValue
|
||||
: `${currentPoint} ${currentPoint > 1 ? "points" : "point"}`}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{activity.new_value ? activity.new_value : activity?.old_value || ""}
|
||||
{showIssue && (activity.new_value ? ` to ` : ` from `)}
|
||||
{showIssue && <IssueLink activityId={activityId} />}.
|
||||
</>
|
||||
|
@ -44,11 +44,11 @@ export const ReactionSelector: React.FC<Props> = (props) => {
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
>
|
||||
<Popover.Panel
|
||||
className={`absolute -left-2 z-10 bg-custom-sidebar-background-100 ${
|
||||
className={`absolute z-10 bg-custom-sidebar-background-100 ${
|
||||
position === "top" ? "-top-12" : "-bottom-12"
|
||||
}`}
|
||||
>
|
||||
<div className="rounded-md border border-custom-border-200 bg-custom-sidebar-background-100 p-1 shadow-custom-shadow-sm">
|
||||
<div className="rounded-md border border-custom-border-200 bg-custom-sidebar-background-100 p-1">
|
||||
<div className="flex gap-x-1">
|
||||
{reactionEmojis.map((emoji) => (
|
||||
<button
|
||||
|
@ -6,7 +6,7 @@ import { ChevronRight } from "lucide-react";
|
||||
// types
|
||||
import { TIssue, IIssueDisplayProperties, TIssueMap } from "@plane/types";
|
||||
// ui
|
||||
import { Spinner, Tooltip, ControlLink, DragHandle } from "@plane/ui";
|
||||
import { Spinner, Tooltip, ControlLink, setToast, TOAST_TYPE } from "@plane/ui";
|
||||
// components
|
||||
import { MultipleSelectEntityAction } from "@/components/core";
|
||||
import { IssueProperties } from "@/components/issues/issue-layouts/properties";
|
||||
@ -57,7 +57,6 @@ export const IssueBlock = observer((props: IssueBlockProps) => {
|
||||
} = props;
|
||||
// ref
|
||||
const issueRef = useRef<HTMLDivElement | null>(null);
|
||||
const dragHandleRef = useRef(null);
|
||||
// hooks
|
||||
const { workspaceSlug, projectId } = useAppRouter();
|
||||
const { getProjectIdentifierById } = useProject();
|
||||
@ -78,14 +77,12 @@ export const IssueBlock = observer((props: IssueBlockProps) => {
|
||||
|
||||
useEffect(() => {
|
||||
const element = issueRef.current;
|
||||
const dragHandleElement = dragHandleRef.current;
|
||||
|
||||
if (!element || !dragHandleElement) return;
|
||||
if (!element) return;
|
||||
|
||||
return combine(
|
||||
draggable({
|
||||
element,
|
||||
dragHandle: dragHandleElement,
|
||||
canDrag: () => canDrag,
|
||||
getInitialData: () => ({ id: issueId, type: "ISSUE", groupId }),
|
||||
onDragStart: () => {
|
||||
@ -96,7 +93,7 @@ export const IssueBlock = observer((props: IssueBlockProps) => {
|
||||
},
|
||||
})
|
||||
);
|
||||
}, [issueRef?.current, canDrag, issueId, groupId, dragHandleRef?.current, setIsCurrentBlockDragging]);
|
||||
}, [canDrag, issueId, groupId, setIsCurrentBlockDragging]);
|
||||
|
||||
if (!issue) return null;
|
||||
|
||||
@ -135,20 +132,19 @@ export const IssueBlock = observer((props: IssueBlockProps) => {
|
||||
"bg-custom-background-80": isCurrentBlockDragging,
|
||||
}
|
||||
)}
|
||||
onDragStart={() => {
|
||||
if (!canDrag) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.WARNING,
|
||||
title: "Cannot move issue",
|
||||
message: "Drag and drop is disabled for the current grouping",
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex w-full truncate">
|
||||
<div className="flex flex-grow items-center gap-3 truncate">
|
||||
<div className="flex items-center gap-0.5">
|
||||
{/* drag handle */}
|
||||
<div className="size-4 flex items-center group/drag-handle">
|
||||
<DragHandle
|
||||
ref={dragHandleRef}
|
||||
disabled={!canDrag}
|
||||
className={cn("opacity-0 group-hover/drag-handle:opacity-100", {
|
||||
"opacity-100": isCurrentBlockDragging,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-grow items-center gap-1.5 truncate">
|
||||
<div className="flex items-center gap-2" style={isSubIssue ? { marginLeft } : {}}>
|
||||
{/* select checkbox */}
|
||||
{projectId && canEditIssueProperties && (
|
||||
<Tooltip
|
||||
@ -177,8 +173,14 @@ export const IssueBlock = observer((props: IssueBlockProps) => {
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
{displayProperties && displayProperties?.key && (
|
||||
<div className="flex-shrink-0 text-xs font-medium text-custom-text-300">
|
||||
{projectIdentifier}-{issue.sequence_id}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* sub-issues chevron */}
|
||||
<div className="size-4 grid place-items-center flex-shrink-0" style={isSubIssue ? { marginLeft } : {}}>
|
||||
<div className="size-4 grid place-items-center flex-shrink-0">
|
||||
{subIssuesCount > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
@ -194,11 +196,6 @@ export const IssueBlock = observer((props: IssueBlockProps) => {
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{displayProperties && displayProperties?.key && (
|
||||
<div className="flex-shrink-0 text-xs font-medium text-custom-text-300">
|
||||
{projectIdentifier}-{issue.sequence_id}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{issue?.tempId !== undefined && (
|
||||
<div className="absolute left-0 top-0 z-[99999] h-full w-full animate-pulse bg-custom-background-100/20" />
|
||||
@ -206,7 +203,12 @@ export const IssueBlock = observer((props: IssueBlockProps) => {
|
||||
</div>
|
||||
|
||||
{issue?.is_draft ? (
|
||||
<Tooltip tooltipContent={issue.name} isMobile={isMobile} position="top-left">
|
||||
<Tooltip
|
||||
tooltipContent={issue.name}
|
||||
isMobile={isMobile}
|
||||
position="top-left"
|
||||
disabled={isCurrentBlockDragging}
|
||||
>
|
||||
<p className="truncate">{issue.name}</p>
|
||||
</Tooltip>
|
||||
) : (
|
||||
|
@ -170,7 +170,7 @@ const GroupByList: React.FC<IGroupByList> = observer((props) => {
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<IssueBulkOperationsRoot />
|
||||
<IssueBulkOperationsRoot selectionHelpers={helpers} />
|
||||
</>
|
||||
)}
|
||||
</MultipleSelectGroup>
|
||||
|
@ -83,7 +83,7 @@ export const HeaderGroupByCard = observer((props: IHeaderGroupByCard) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="group/list-header relative w-full flex-shrink-0 flex items-center gap-2.5 py-1.5 pl-3.5">
|
||||
<div className="group/list-header relative w-full flex-shrink-0 flex items-center gap-2 py-1.5">
|
||||
{canSelectIssues && (
|
||||
<div className="flex-shrink-0 flex items-center w-3.5">
|
||||
<MultipleSelectGroupAction
|
||||
@ -98,7 +98,7 @@ export const HeaderGroupByCard = observer((props: IHeaderGroupByCard) => {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-shrink-0 grid place-items-center overflow-hidden pl-3">
|
||||
<div className="flex-shrink-0 grid place-items-center overflow-hidden">
|
||||
{icon ?? <CircleDashed className="size-3.5" strokeWidth={2} />}
|
||||
</div>
|
||||
|
||||
|
@ -193,7 +193,7 @@ export const ListGroup = observer((props: Props) => {
|
||||
"border-custom-error-200": isDraggingOverColumn && !!group.isDropDisabled,
|
||||
})}
|
||||
>
|
||||
<div className="sticky top-0 z-[2] w-full flex-shrink-0 border-b border-custom-border-200 bg-custom-background-90 px-3 py-1">
|
||||
<div className="sticky top-0 z-[2] w-full flex-shrink-0 border-b border-custom-border-200 bg-custom-background-90 pl-2 pr-3 py-1">
|
||||
<HeaderGroupByCard
|
||||
groupID={group.id}
|
||||
icon={group.icon}
|
||||
|
@ -152,7 +152,7 @@ export const ListQuickAddIssueForm: FC<IListQuickAddIssueForm> = observer((props
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="flex w-full cursor-pointer items-center gap-2.5 p-6 py-3 text-custom-text-350 hover:text-custom-text-300"
|
||||
className="flex w-full cursor-pointer items-center gap-2 px-2 py-3 text-custom-text-350 hover:text-custom-text-300"
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
<PlusIcon className="h-3.5 w-3.5 stroke-2" />
|
||||
|
@ -107,7 +107,7 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<IssueBulkOperationsRoot />
|
||||
<IssueBulkOperationsRoot selectionHelpers={helpers} />
|
||||
</>
|
||||
)}
|
||||
</MultipleSelectGroup>
|
||||
|
@ -495,7 +495,7 @@ export const handleGroupDragDrop = async (
|
||||
// update updatedIssue values based on the source and destination groupIds
|
||||
if (source.groupId && destination.groupId && source.groupId !== destination.groupId) {
|
||||
const groupKey = ISSUE_FILTER_DEFAULT_DATA[groupBy];
|
||||
let groupValue = clone(sourceIssue[groupKey]);
|
||||
let groupValue: any = clone(sourceIssue[groupKey]);
|
||||
|
||||
// If groupValues is an array, remove source groupId and add destination groupId
|
||||
if (Array.isArray(groupValue)) {
|
||||
@ -515,7 +515,7 @@ export const handleGroupDragDrop = async (
|
||||
// update updatedIssue values based on the source and destination subGroupIds
|
||||
if (subGroupBy && source.subGroupId && destination.subGroupId && source.subGroupId !== destination.subGroupId) {
|
||||
const subGroupKey = ISSUE_FILTER_DEFAULT_DATA[subGroupBy];
|
||||
let subGroupValue = clone(sourceIssue[subGroupKey]);
|
||||
let subGroupValue: any = clone(sourceIssue[subGroupKey]);
|
||||
|
||||
// If subGroupValue is an array, remove source subGroupId and add destination subGroupId
|
||||
if (Array.isArray(subGroupValue)) {
|
||||
|
@ -5,22 +5,30 @@ import { observer } from "mobx-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { ChevronDown, ChevronRight, Plus } from "lucide-react";
|
||||
import { Disclosure, Transition } from "@headlessui/react";
|
||||
// types
|
||||
import { IProject } from "@plane/types";
|
||||
// hooks
|
||||
// ui
|
||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { CreateProjectModal, ProjectSidebarListItem } from "@/components/project";
|
||||
// constants
|
||||
import { EUserWorkspaceRoles } from "@/constants/workspace";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { orderJoinedProjects } from "@/helpers/project.helper";
|
||||
import { copyUrlToClipboard } from "@/helpers/string.helper";
|
||||
// hooks
|
||||
import { useAppTheme, useCommandPalette, useEventTracker, useProject, useUser } from "@/hooks/store";
|
||||
// ui
|
||||
// components
|
||||
// helpers
|
||||
// constants
|
||||
|
||||
export const ProjectSidebarList: FC = observer(() => {
|
||||
// get local storage data for isFavoriteProjectsListOpen and isAllProjectsListOpen
|
||||
const isFavProjectsListOpenInLocalStorage = localStorage.getItem("isFavoriteProjectsListOpen");
|
||||
const isAllProjectsListOpenInLocalStorage = localStorage.getItem("isAllProjectsListOpen");
|
||||
// states
|
||||
const [isFavoriteProjectsListOpen, setIsFavoriteProjectsListOpen] = useState(
|
||||
isFavProjectsListOpenInLocalStorage === "true"
|
||||
);
|
||||
const [isAllProjectsListOpen, setIsAllProjectsListOpen] = useState(isAllProjectsListOpenInLocalStorage === "true");
|
||||
const [isFavoriteProjectCreate, setIsFavoriteProjectCreate] = useState(false);
|
||||
const [isProjectModalOpen, setIsProjectModalOpen] = useState(false);
|
||||
const [isScrolled, setIsScrolled] = useState(false); // scroll animation state
|
||||
@ -122,6 +130,16 @@ export const ProjectSidebarList: FC = observer(() => {
|
||||
);
|
||||
}, [containerRef]);
|
||||
|
||||
const toggleListDisclosure = (isOpen: boolean, type: "all" | "favorite") => {
|
||||
if (type === "all") {
|
||||
setIsAllProjectsListOpen(isOpen);
|
||||
localStorage.setItem("isAllProjectsListOpen", isOpen.toString());
|
||||
} else {
|
||||
setIsFavoriteProjectsListOpen(isOpen);
|
||||
localStorage.setItem("isFavoriteProjectsListOpen", isOpen.toString());
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{workspaceSlug && (
|
||||
@ -147,42 +165,48 @@ export const ProjectSidebarList: FC = observer(() => {
|
||||
>
|
||||
<div>
|
||||
{favoriteProjects && favoriteProjects.length > 0 && (
|
||||
<Disclosure as="div" className="flex flex-col" defaultOpen>
|
||||
{({ open }) => (
|
||||
<>
|
||||
{!isCollapsed && (
|
||||
<div className="group flex w-full items-center justify-between rounded p-1.5 text-xs text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80">
|
||||
<Disclosure.Button
|
||||
as="button"
|
||||
type="button"
|
||||
className="group flex w-full items-center gap-1 whitespace-nowrap rounded px-1.5 text-left text-sm font-semibold text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80"
|
||||
>
|
||||
Favorites
|
||||
{open ? <ChevronDown className="h-3.5 w-3.5" /> : <ChevronRight className="h-3.5 w-3.5" />}
|
||||
</Disclosure.Button>
|
||||
{isAuthorizedUser && (
|
||||
<button
|
||||
className="opacity-0 group-hover:opacity-100"
|
||||
onClick={() => {
|
||||
setTrackElement("APP_SIDEBAR_FAVORITES_BLOCK");
|
||||
setIsFavoriteProjectCreate(true);
|
||||
setIsProjectModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<Plus className="h-3 w-3" />
|
||||
</button>
|
||||
<Disclosure as="div" className="flex flex-col" defaultOpen={isFavoriteProjectCreate}>
|
||||
<>
|
||||
{!isCollapsed && (
|
||||
<div className="group flex w-full items-center justify-between rounded p-1.5 text-xs text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80">
|
||||
<Disclosure.Button
|
||||
as="button"
|
||||
type="button"
|
||||
className="group flex w-full items-center gap-1 whitespace-nowrap rounded px-1.5 text-left text-sm font-semibold text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80"
|
||||
onClick={() => toggleListDisclosure(!isFavoriteProjectsListOpen, "favorite")}
|
||||
>
|
||||
Favorites
|
||||
{isFavoriteProjectsListOpen ? (
|
||||
<ChevronDown className="h-3.5 w-3.5" />
|
||||
) : (
|
||||
<ChevronRight className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<Transition
|
||||
enter="transition duration-100 ease-out"
|
||||
enterFrom="transform scale-95 opacity-0"
|
||||
enterTo="transform scale-100 opacity-100"
|
||||
leave="transition duration-75 ease-out"
|
||||
leaveFrom="transform scale-100 opacity-100"
|
||||
leaveTo="transform scale-95 opacity-0"
|
||||
>
|
||||
<Disclosure.Panel as="div" className="space-y-2">
|
||||
</Disclosure.Button>
|
||||
{isAuthorizedUser && (
|
||||
<button
|
||||
className="opacity-0 group-hover:opacity-100"
|
||||
onClick={() => {
|
||||
setTrackElement("APP_SIDEBAR_FAVORITES_BLOCK");
|
||||
setIsFavoriteProjectCreate(true);
|
||||
setIsProjectModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<Plus className="h-3 w-3" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<Transition
|
||||
show={isFavoriteProjectsListOpen}
|
||||
enter="transition duration-100 ease-out"
|
||||
enterFrom="transform scale-95 opacity-0"
|
||||
enterTo="transform scale-100 opacity-100"
|
||||
leave="transition duration-75 ease-out"
|
||||
leaveFrom="transform scale-100 opacity-100"
|
||||
leaveTo="transform scale-95 opacity-0"
|
||||
>
|
||||
{isFavoriteProjectsListOpen && (
|
||||
<Disclosure.Panel as="div" className={`space-y-2`} static>
|
||||
{favoriteProjects.map((projectId, index) => (
|
||||
<ProjectSidebarListItem
|
||||
key={projectId}
|
||||
@ -195,50 +219,56 @@ export const ProjectSidebarList: FC = observer(() => {
|
||||
/>
|
||||
))}
|
||||
</Disclosure.Panel>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
)}
|
||||
</Transition>
|
||||
</>
|
||||
</Disclosure>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
{joinedProjects && joinedProjects.length > 0 && (
|
||||
<Disclosure as="div" className="flex flex-col" defaultOpen>
|
||||
{({ open }) => (
|
||||
<>
|
||||
{!isCollapsed && (
|
||||
<div className="group flex w-full items-center justify-between rounded p-1.5 text-xs text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80">
|
||||
<Disclosure.Button
|
||||
as="button"
|
||||
type="button"
|
||||
className="group flex w-full items-center gap-1 whitespace-nowrap rounded px-1.5 text-left text-sm font-semibold text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80"
|
||||
>
|
||||
Your projects
|
||||
{open ? <ChevronDown className="h-3.5 w-3.5" /> : <ChevronRight className="h-3.5 w-3.5" />}
|
||||
</Disclosure.Button>
|
||||
{isAuthorizedUser && (
|
||||
<button
|
||||
className="opacity-0 group-hover:opacity-100"
|
||||
onClick={() => {
|
||||
setTrackElement("Sidebar");
|
||||
setIsFavoriteProjectCreate(false);
|
||||
setIsProjectModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<Plus className="h-3 w-3" />
|
||||
</button>
|
||||
<Disclosure as="div" className="flex flex-col" defaultOpen={isAllProjectsListOpen}>
|
||||
<>
|
||||
{!isCollapsed && (
|
||||
<div className="group flex w-full items-center justify-between rounded p-1.5 text-xs text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80">
|
||||
<Disclosure.Button
|
||||
as="button"
|
||||
type="button"
|
||||
className="group flex w-full items-center gap-1 whitespace-nowrap rounded px-1.5 text-left text-sm font-semibold text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80"
|
||||
onClick={() => toggleListDisclosure(!isAllProjectsListOpen, "all")}
|
||||
>
|
||||
Your projects
|
||||
{isAllProjectsListOpen ? (
|
||||
<ChevronDown className="h-3.5 w-3.5" />
|
||||
) : (
|
||||
<ChevronRight className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<Transition
|
||||
enter="transition duration-100 ease-out"
|
||||
enterFrom="transform scale-95 opacity-0"
|
||||
enterTo="transform scale-100 opacity-100"
|
||||
leave="transition duration-75 ease-out"
|
||||
leaveFrom="transform scale-100 opacity-100"
|
||||
leaveTo="transform scale-95 opacity-0"
|
||||
>
|
||||
<Disclosure.Panel as="div">
|
||||
</Disclosure.Button>
|
||||
{isAuthorizedUser && (
|
||||
<button
|
||||
className="opacity-0 group-hover:opacity-100"
|
||||
onClick={() => {
|
||||
setTrackElement("Sidebar");
|
||||
setIsFavoriteProjectCreate(false);
|
||||
setIsProjectModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<Plus className="h-3 w-3" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<Transition
|
||||
show={isAllProjectsListOpen}
|
||||
enter="transition duration-100 ease-out"
|
||||
enterFrom="transform scale-95 opacity-0"
|
||||
enterTo="transform scale-100 opacity-100"
|
||||
leave="transition duration-75 ease-out"
|
||||
leaveFrom="transform scale-100 opacity-100"
|
||||
leaveTo="transform scale-95 opacity-0"
|
||||
>
|
||||
{isAllProjectsListOpen && (
|
||||
<Disclosure.Panel as="div" static>
|
||||
{joinedProjects.map((projectId, index) => (
|
||||
<ProjectSidebarListItem
|
||||
key={projectId}
|
||||
@ -250,9 +280,9 @@ export const ProjectSidebarList: FC = observer(() => {
|
||||
/>
|
||||
))}
|
||||
</Disclosure.Panel>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
)}
|
||||
</Transition>
|
||||
</>
|
||||
</Disclosure>
|
||||
)}
|
||||
</div>
|
||||
|
@ -233,6 +233,7 @@ export const CreateUpdateStateInline: React.FC<Props> = observer((props) => {
|
||||
hasError={Boolean(errors.name)}
|
||||
placeholder="Name"
|
||||
className="w-full"
|
||||
autoFocus
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
@ -9,12 +9,12 @@ export type UseIntersectionObserverProps = {
|
||||
|
||||
export const useIntersectionObserver = (
|
||||
containerRef: RefObject<HTMLDivElement>,
|
||||
elementRef: RefObject<HTMLDivElement>,
|
||||
elementRef: HTMLDivElement | null,
|
||||
callback: () => void,
|
||||
rootMargin?: string
|
||||
) => {
|
||||
useEffect(() => {
|
||||
if (elementRef.current) {
|
||||
if (elementRef) {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[entries.length - 1].isIntersecting) {
|
||||
@ -26,16 +26,16 @@ export const useIntersectionObserver = (
|
||||
rootMargin,
|
||||
}
|
||||
);
|
||||
observer.observe(elementRef.current);
|
||||
observer.observe(elementRef);
|
||||
return () => {
|
||||
if (elementRef.current) {
|
||||
if (elementRef) {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
observer.unobserve(elementRef.current);
|
||||
observer.unobserve(elementRef);
|
||||
}
|
||||
};
|
||||
}
|
||||
// while removing the current from the refs, the observer is not not working as expected
|
||||
// fix this eslint warning with caution
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [rootMargin, callback, elementRef.current, containerRef.current]);
|
||||
}, [rootMargin, callback, elementRef, containerRef.current]);
|
||||
};
|
||||
|
@ -33,6 +33,7 @@ export const useMultipleSelect = (props: Props) => {
|
||||
const router = useRouter();
|
||||
// store hooks
|
||||
const {
|
||||
selectedEntityIds,
|
||||
updateSelectedEntityDetails,
|
||||
bulkUpdateSelectedEntityDetails,
|
||||
getActiveEntityDetails,
|
||||
@ -45,6 +46,7 @@ export const useMultipleSelect = (props: Props) => {
|
||||
clearSelection,
|
||||
getIsEntitySelected,
|
||||
getIsEntityActive,
|
||||
getEntityDetailsFromEntityID,
|
||||
} = useMultipleSelectStore();
|
||||
|
||||
const groups = useMemo(() => Object.keys(entities), [entities]);
|
||||
@ -248,10 +250,6 @@ export const useMultipleSelect = (props: Props) => {
|
||||
(groupID: string) => {
|
||||
const groupEntities = entitiesList.filter((entity) => entity.groupID === groupID);
|
||||
const groupSelectionStatus = isGroupSelected(groupID);
|
||||
// groupEntities.map((entity) => {
|
||||
// console.log("group click");
|
||||
// handleEntitySelection(entity, false, groupSelectionStatus === "empty" ? "force-add" : "force-remove");
|
||||
// });
|
||||
handleEntitySelection(groupEntities, false, groupSelectionStatus === "empty" ? "force-add" : "force-remove");
|
||||
},
|
||||
[entitiesList, handleEntitySelection, isGroupSelected]
|
||||
@ -346,6 +344,19 @@ export const useMultipleSelect = (props: Props) => {
|
||||
};
|
||||
}, [clearSelection, router.events]);
|
||||
|
||||
// when entities list change, remove entityIds from the selected entities array, which are not present in the new list
|
||||
useEffect(() => {
|
||||
selectedEntityIds.map((entityID) => {
|
||||
const isEntityPresent = entitiesList.find((en) => en.entityID === entityID);
|
||||
if (!isEntityPresent) {
|
||||
const entityDetails = getEntityDetailsFromEntityID(entityID);
|
||||
if (entityDetails) {
|
||||
handleEntitySelection(entityDetails);
|
||||
}
|
||||
}
|
||||
});
|
||||
}, [entitiesList, getEntityDetailsFromEntityID, handleEntitySelection, selectedEntityIds]);
|
||||
|
||||
/**
|
||||
* @description helper functions for selection
|
||||
*/
|
||||
|
@ -54,11 +54,9 @@ export const AppProvider: FC<IAppProvider> = observer((props) => {
|
||||
posthogAPIKey={config?.posthog_api_key || undefined}
|
||||
posthogHost={config?.posthog_host || undefined}
|
||||
>
|
||||
<SWRConfig value={SWR_CONFIG}>
|
||||
{/* TODO: Need to handle custom themes for toast */}
|
||||
<Toast theme={resolveGeneralTheme(resolvedTheme)} />
|
||||
{children}
|
||||
</SWRConfig>
|
||||
{/* TODO: Need to handle custom themes for toast */}
|
||||
<Toast theme={resolveGeneralTheme(resolvedTheme)} />
|
||||
<SWRConfig value={SWR_CONFIG}>{children}</SWRConfig>
|
||||
</PostHogProvider>
|
||||
</CrispWrapper>
|
||||
</StoreWrapper>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ReactElement, useEffect } from "react";
|
||||
import { ReactElement } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useRouter } from "next/router";
|
||||
// components
|
||||
@ -11,7 +11,7 @@ import { EmptyStateType } from "@/constants/empty-state";
|
||||
// helpers
|
||||
import { EInboxIssueCurrentTab } from "@/helpers/inbox.helper";
|
||||
// hooks
|
||||
import { useProject, useProjectInbox } from "@/hooks/store";
|
||||
import { useProject } from "@/hooks/store";
|
||||
// layouts
|
||||
import { AppLayout } from "@/layouts/app-layout";
|
||||
// types
|
||||
@ -23,12 +23,6 @@ const ProjectInboxPage: NextPageWithLayout = observer(() => {
|
||||
const { workspaceSlug, projectId, currentTab: navigationTab, inboxIssueId } = router.query;
|
||||
// hooks
|
||||
const { currentProjectDetails } = useProject();
|
||||
const { currentTab, handleCurrentTab } = useProjectInbox();
|
||||
|
||||
useEffect(() => {
|
||||
if (navigationTab && currentTab != navigationTab)
|
||||
handleCurrentTab(navigationTab === "open" ? EInboxIssueCurrentTab.OPEN : EInboxIssueCurrentTab.CLOSED);
|
||||
}, [currentTab, navigationTab, handleCurrentTab]);
|
||||
|
||||
// No access to inbox
|
||||
if (currentProjectDetails?.inbox_view === false)
|
||||
@ -44,6 +38,12 @@ const ProjectInboxPage: NextPageWithLayout = observer(() => {
|
||||
// derived values
|
||||
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Inbox` : "Plane - Inbox";
|
||||
|
||||
const currentNavigationTab = navigationTab
|
||||
? navigationTab === "open"
|
||||
? EInboxIssueCurrentTab.OPEN
|
||||
: EInboxIssueCurrentTab.CLOSED
|
||||
: undefined;
|
||||
|
||||
if (!workspaceSlug || !projectId) return <></>;
|
||||
|
||||
return (
|
||||
@ -55,6 +55,7 @@ const ProjectInboxPage: NextPageWithLayout = observer(() => {
|
||||
projectId={projectId.toString()}
|
||||
inboxIssueId={inboxIssueId?.toString() || undefined}
|
||||
inboxAccessible={currentProjectDetails?.inbox_view || false}
|
||||
navigationTab={currentNavigationTab}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -321,8 +321,6 @@ export class ProjectInboxStore implements IProjectInboxStore {
|
||||
(this.inboxIssuePaginationInfo?.total_results &&
|
||||
this.inboxIssueIds.length < this.inboxIssuePaginationInfo?.total_results))
|
||||
) {
|
||||
this.loader = "pagination-loading";
|
||||
|
||||
const queryParams = this.inboxIssueQueryParams(
|
||||
this.inboxFilters,
|
||||
this.inboxSorting,
|
||||
@ -332,7 +330,6 @@ export class ProjectInboxStore implements IProjectInboxStore {
|
||||
const { results, ...paginationInfo } = await this.inboxIssueService.list(workspaceSlug, projectId, queryParams);
|
||||
|
||||
runInAction(() => {
|
||||
this.loader = undefined;
|
||||
set(this, "inboxIssuePaginationInfo", paginationInfo);
|
||||
if (results && results.length > 0) {
|
||||
const issueIds = results.map((value) => value?.issue?.id);
|
||||
@ -343,7 +340,6 @@ export class ProjectInboxStore implements IProjectInboxStore {
|
||||
} else set(this, ["inboxIssuePaginationInfo", "next_page_results"], false);
|
||||
} catch (error) {
|
||||
console.error("Error fetching the inbox issues", error);
|
||||
this.loader = undefined;
|
||||
this.error = {
|
||||
message: "Error fetching the paginated inbox issues please try again later.",
|
||||
status: "pagination-error",
|
||||
|
@ -4,10 +4,10 @@ import sortBy from "lodash/sortBy";
|
||||
import uniq from "lodash/uniq";
|
||||
import update from "lodash/update";
|
||||
import { action, makeObservable, observable, runInAction } from "mobx";
|
||||
import { TIssueActivityComment, TIssueActivity, TIssueActivityMap, TIssueActivityIdMap } from "@plane/types";
|
||||
// services
|
||||
import { IssueActivityService } from "@/services/issue";
|
||||
// types
|
||||
import { TIssueActivityComment, TIssueActivity, TIssueActivityMap, TIssueActivityIdMap } from "@plane/types";
|
||||
import { IIssueDetail } from "./root.store";
|
||||
|
||||
export type TActivityLoader = "fetch" | "mutate" | undefined;
|
||||
@ -117,10 +117,10 @@ export class IssueActivityStore implements IIssueActivityStore {
|
||||
this.loader = loaderType;
|
||||
|
||||
let props = {};
|
||||
const _activityIds = this.getActivitiesByIssueId(issueId);
|
||||
if (_activityIds && _activityIds.length > 0) {
|
||||
const _activity = this.getActivityById(_activityIds[_activityIds.length - 1]);
|
||||
if (_activity) props = { created_at__gt: _activity.created_at };
|
||||
const currentActivityIds = this.getActivitiesByIssueId(issueId);
|
||||
if (currentActivityIds && currentActivityIds.length > 0) {
|
||||
const currentActivity = this.getActivityById(currentActivityIds[currentActivityIds.length - 1]);
|
||||
if (currentActivity) props = { created_at__gt: currentActivity.created_at };
|
||||
}
|
||||
|
||||
const activities = await this.issueActivityService.getIssueActivities(workspaceSlug, projectId, issueId, props);
|
||||
@ -128,9 +128,9 @@ export class IssueActivityStore implements IIssueActivityStore {
|
||||
const activityIds = activities.map((activity) => activity.id);
|
||||
|
||||
runInAction(() => {
|
||||
update(this.activities, issueId, (_activityIds) => {
|
||||
if (!_activityIds) return activityIds;
|
||||
return uniq(concat(_activityIds, activityIds));
|
||||
update(this.activities, issueId, (currentActivityIds) => {
|
||||
if (!currentActivityIds) return activityIds;
|
||||
return uniq(concat(currentActivityIds, activityIds));
|
||||
});
|
||||
activities.forEach((activity) => {
|
||||
set(this.activityMap, activity.id, activity);
|
||||
|
@ -19,6 +19,7 @@ export type IMultipleSelectStore = {
|
||||
getPreviousActiveEntity: () => TEntityDetails | null;
|
||||
getNextActiveEntity: () => TEntityDetails | null;
|
||||
getActiveEntityDetails: () => TEntityDetails | null;
|
||||
getEntityDetailsFromEntityID: (entityID: string) => TEntityDetails | null;
|
||||
// entity actions
|
||||
updateSelectedEntityDetails: (entityDetails: TEntityDetails, action: "add" | "remove") => void;
|
||||
bulkUpdateSelectedEntityDetails: (entitiesList: TEntityDetails[], action: "add" | "remove") => void;
|
||||
@ -119,6 +120,16 @@ export class MultipleSelectStore implements IMultipleSelectStore {
|
||||
*/
|
||||
getActiveEntityDetails = computedFn(() => this.activeEntityDetails);
|
||||
|
||||
/**
|
||||
* @description get the entity details from entityID
|
||||
* @param {string} entityID
|
||||
* @returns {TEntityDetails | null}
|
||||
*/
|
||||
getEntityDetailsFromEntityID = computedFn(
|
||||
(entityID: string): TEntityDetails | null =>
|
||||
this.selectedEntityDetails.find((en) => en.entityID === entityID) ?? null
|
||||
);
|
||||
|
||||
// entity actions
|
||||
/**
|
||||
* @description add or remove entities
|
||||
@ -159,8 +170,11 @@ export class MultipleSelectStore implements IMultipleSelectStore {
|
||||
if (entitiesList.length > 0) this.updateLastSelectedEntityDetails(entitiesList[entitiesList.length - 1]);
|
||||
});
|
||||
} else {
|
||||
const newEntities = differenceWith(this.selectedEntityDetails, entitiesList, (obj1, obj2) =>
|
||||
isEqual(obj1.entityID, obj2.entityID)
|
||||
);
|
||||
runInAction(() => {
|
||||
this.selectedEntityDetails = differenceWith(this.selectedEntityDetails, entitiesList, isEqual);
|
||||
this.selectedEntityDetails = newEntities;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user