diff --git a/apiserver/plane/api/serializers/issue.py b/apiserver/plane/api/serializers/issue.py
index 1f4d814a4..6cd06a767 100644
--- a/apiserver/plane/api/serializers/issue.py
+++ b/apiserver/plane/api/serializers/issue.py
@@ -680,7 +680,7 @@ class IssueLiteSerializer(BaseSerializer):
class IssuePublicSerializer(BaseSerializer):
project_detail = ProjectLiteSerializer(read_only=True, source="project")
state_detail = StateLiteSerializer(read_only=True, source="state")
- issue_reactions = IssueReactionLiteSerializer(read_only=True, many=True)
+ reactions = IssueReactionLiteSerializer(read_only=True, many=True, source="issue_reactions")
votes = IssueVoteSerializer(read_only=True, many=True)
class Meta:
@@ -697,12 +697,13 @@ class IssuePublicSerializer(BaseSerializer):
"workspace",
"priority",
"target_date",
- "issue_reactions",
+ "reactions",
"votes",
]
read_only_fields = fields
+
class IssueSubscriberSerializer(BaseSerializer):
class Meta:
model = IssueSubscriber
diff --git a/apiserver/plane/api/views/cycle.py b/apiserver/plane/api/views/cycle.py
index a3d89fa81..3dca6c312 100644
--- a/apiserver/plane/api/views/cycle.py
+++ b/apiserver/plane/api/views/cycle.py
@@ -191,11 +191,10 @@ class CycleViewSet(BaseViewSet):
workspace__slug=slug,
project_id=project_id,
)
- .annotate(first_name=F("assignees__first_name"))
- .annotate(last_name=F("assignees__last_name"))
+ .annotate(display_name=F("assignees__display_name"))
.annotate(assignee_id=F("assignees__id"))
.annotate(avatar=F("assignees__avatar"))
- .values("first_name", "last_name", "assignee_id", "avatar")
+ .values("display_name", "assignee_id", "avatar")
.annotate(total_issues=Count("assignee_id"))
.annotate(
completed_issues=Count(
@@ -209,7 +208,7 @@ class CycleViewSet(BaseViewSet):
filter=Q(completed_at__isnull=True),
)
)
- .order_by("first_name", "last_name")
+ .order_by("display_name")
)
label_distribution = (
diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py
index cbcd40f04..74b574423 100644
--- a/apiserver/plane/api/views/issue.py
+++ b/apiserver/plane/api/views/issue.py
@@ -28,7 +28,7 @@ from django.conf import settings
from rest_framework.response import Response
from rest_framework import status
from rest_framework.parsers import MultiPartParser, FormParser
-from rest_framework.permissions import AllowAny
+from rest_framework.permissions import AllowAny, IsAuthenticated
from sentry_sdk import capture_exception
# Module imports
@@ -1504,7 +1504,7 @@ class CommentReactionViewSet(BaseViewSet):
{
"reaction": str(reaction_code),
"identifier": str(comment_reaction.id),
- "comment_id": str(comment_id)
+ "comment_id": str(comment_id),
}
),
)
@@ -1532,6 +1532,18 @@ class IssueCommentPublicViewSet(BaseViewSet):
"workspace__id",
]
+ def get_permissions(self):
+ if self.action in ["list", "retrieve"]:
+ self.permission_classes = [
+ AllowAny,
+ ]
+ else:
+ self.permission_classes = [
+ IsAuthenticated,
+ ]
+
+ return super(IssueCommentPublicViewSet, self).get_permissions()
+
def get_queryset(self):
project_deploy_board = ProjectDeployBoard.objects.get(
workspace__slug=self.kwargs.get("slug"),
@@ -1741,7 +1753,7 @@ class IssueReactionPublicViewSet(BaseViewSet):
issue_id=str(self.kwargs.get("issue_id", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=None,
- )
+ )
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except ProjectDeployBoard.DoesNotExist:
@@ -1855,7 +1867,7 @@ class CommentReactionPublicViewSet(BaseViewSet):
issue_id=None,
project_id=str(self.kwargs.get("project_id", None)),
current_instance=None,
- )
+ )
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except IssueComment.DoesNotExist:
@@ -1903,7 +1915,7 @@ class CommentReactionPublicViewSet(BaseViewSet):
{
"reaction": str(reaction_code),
"identifier": str(comment_reaction.id),
- "comment_id": str(comment_id)
+ "comment_id": str(comment_id),
}
),
)
@@ -1953,13 +1965,13 @@ class IssueVotePublicViewSet(BaseViewSet):
issue_vote.vote = request.data.get("vote", 1)
issue_vote.save()
issue_activity.delay(
- type="issue_vote.activity.created",
- requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder),
- actor_id=str(self.request.user.id),
- issue_id=str(self.kwargs.get("issue_id", None)),
- project_id=str(self.kwargs.get("project_id", None)),
- current_instance=None,
- )
+ type="issue_vote.activity.created",
+ requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder),
+ actor_id=str(self.request.user.id),
+ issue_id=str(self.kwargs.get("issue_id", None)),
+ project_id=str(self.kwargs.get("project_id", None)),
+ current_instance=None,
+ )
serializer = IssueVoteSerializer(issue_vote)
return Response(serializer.data, status=status.HTTP_201_CREATED)
except Exception as e:
@@ -2170,4 +2182,3 @@ class ProjectIssuesPublicEndpoint(BaseAPIView):
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
-
diff --git a/apiserver/plane/db/migrations/0042_alter_analyticview_created_by_and_more.py b/apiserver/plane/db/migrations/0042_alter_analyticview_created_by_and_more.py
index f7d6a979d..62f08038c 100644
--- a/apiserver/plane/db/migrations/0042_alter_analyticview_created_by_and_more.py
+++ b/apiserver/plane/db/migrations/0042_alter_analyticview_created_by_and_more.py
@@ -3,7 +3,7 @@
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
-
+import uuid
def update_user_timezones(apps, schema_editor):
UserModel = apps.get_model("db", "User")
@@ -31,5 +31,38 @@ class Migration(migrations.Migration):
name='title',
field=models.CharField(blank=True, max_length=255, null=True),
),
- migrations.RunPython(update_user_timezones)
+ migrations.RunPython(update_user_timezones),
+ migrations.AlterUniqueTogether(
+ name='issuevote',
+ unique_together=set(),
+ ),
+ migrations.AlterField(
+ model_name='issuevote',
+ name='vote',
+ field=models.IntegerField(choices=[(-1, 'DOWNVOTE'), (1, 'UPVOTE')], default=1),
+ ),
+ migrations.AlterUniqueTogether(
+ name='issuevote',
+ unique_together={('issue', 'actor', 'vote')},
+ ),
+ migrations.CreateModel(
+ name='ProjectPublicMember',
+ 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)),
+ ('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')),
+ ('member', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='public_project_members', to=settings.AUTH_USER_MODEL)),
+ ('project', models.ForeignKey(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': 'Project Public Member',
+ 'verbose_name_plural': 'Project Public Members',
+ 'db_table': 'project_public_members',
+ 'ordering': ('-created_at',),
+ 'unique_together': {('project', 'member')},
+ },
+ ),
]
diff --git a/apiserver/plane/db/migrations/0042_alter_issuevote_unique_together_alter_issuevote_vote_and_more.py b/apiserver/plane/db/migrations/0042_alter_issuevote_unique_together_alter_issuevote_vote_and_more.py
deleted file mode 100644
index d8063acc0..000000000
--- a/apiserver/plane/db/migrations/0042_alter_issuevote_unique_together_alter_issuevote_vote_and_more.py
+++ /dev/null
@@ -1,26 +0,0 @@
-# Generated by Django 4.2.3 on 2023-08-29 07:58
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('db', '0041_cycle_sort_order_issuecomment_access_and_more'),
- ]
-
- operations = [
- migrations.AlterUniqueTogether(
- name='issuevote',
- unique_together=set(),
- ),
- migrations.AlterField(
- model_name='issuevote',
- name='vote',
- field=models.IntegerField(choices=[(-1, 'DOWNVOTE'), (1, 'UPVOTE')], default=1),
- ),
- migrations.AlterUniqueTogether(
- name='issuevote',
- unique_together={('issue', 'actor', 'vote')},
- ),
- ]
diff --git a/apiserver/plane/db/models/issue.py b/apiserver/plane/db/models/issue.py
index 1633cbaf9..8f085b2a2 100644
--- a/apiserver/plane/db/models/issue.py
+++ b/apiserver/plane/db/models/issue.py
@@ -293,7 +293,7 @@ class IssueComment(ProjectBaseModel):
comment_json = models.JSONField(blank=True, default=dict)
comment_html = models.TextField(blank=True, default="
")
attachments = ArrayField(models.URLField(), size=10, blank=True, default=list)
- issue = models.ForeignKey(Issue, on_delete=models.CASCADE)
+ issue = models.ForeignKey(Issue, on_delete=models.CASCADE, related_name="issue_comments")
# System can also create comment
actor = models.ForeignKey(
settings.AUTH_USER_MODEL,
diff --git a/apps/app/components/gantt-chart/helpers/draggable.tsx b/apps/app/components/gantt-chart/helpers/draggable.tsx
index 20423ff59..b665bf5d3 100644
--- a/apps/app/components/gantt-chart/helpers/draggable.tsx
+++ b/apps/app/components/gantt-chart/helpers/draggable.tsx
@@ -73,9 +73,11 @@ export const ChartDraggable: React.FC = ({
};
// handle block resize from the left end
- const handleBlockLeftResize = () => {
+ const handleBlockLeftResize = (e: React.MouseEvent) => {
if (!currentViewData || !resizableRef.current || !block.position) return;
+ if (e.button !== 0) return;
+
const resizableDiv = resizableRef.current;
const columnWidth = currentViewData.data.width;
@@ -126,9 +128,11 @@ export const ChartDraggable: React.FC = ({
};
// handle block resize from the right end
- const handleBlockRightResize = () => {
+ const handleBlockRightResize = (e: React.MouseEvent) => {
if (!currentViewData || !resizableRef.current || !block.position) return;
+ if (e.button !== 0) return;
+
const resizableDiv = resizableRef.current;
const columnWidth = currentViewData.data.width;
@@ -173,6 +177,8 @@ export const ChartDraggable: React.FC = ({
const handleBlockMove = (e: React.MouseEvent) => {
if (!enableBlockMove || !currentViewData || !resizableRef.current || !block.position) return;
+ if (e.button !== 0) return;
+
e.preventDefault();
e.stopPropagation();
@@ -266,7 +272,7 @@ export const ChartDraggable: React.FC = ({
void;
data: IIssue | null;
+ onSubmit?: () => Promise
;
user: ICurrentUserResponse | undefined;
};
-export const DeleteIssueModal: React.FC = ({ isOpen, handleClose, data, user }) => {
+export const DeleteIssueModal: React.FC = ({
+ isOpen,
+ handleClose,
+ data,
+ onSubmit,
+ user,
+}) => {
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const router = useRouter();
@@ -116,6 +123,8 @@ export const DeleteIssueModal: React.FC = ({ isOpen, handleClose, data, u
else mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(data.project, params));
}
+ if (onSubmit) onSubmit();
+
handleClose();
setToastAlert({
title: "Success",
diff --git a/apps/app/components/issues/peek-overview/issue-properties.tsx b/apps/app/components/issues/peek-overview/issue-properties.tsx
index 2c8b4d572..1f2d618ac 100644
--- a/apps/app/components/issues/peek-overview/issue-properties.tsx
+++ b/apps/app/components/issues/peek-overview/issue-properties.tsx
@@ -103,7 +103,7 @@ export const PeekOverviewIssueProperties: React.FC = ({
handleUpdateIssue({ assignees_list: val })}
disabled={readOnly}
/>
@@ -128,23 +128,18 @@ export const PeekOverviewIssueProperties: React.FC = ({
Start date
- {issue.start_date ? (
-
- handleUpdateIssue({
- start_date: val,
- })
- }
- className="bg-custom-background-100"
- wrapperClassName="w-full"
- maxDate={maxDate ?? undefined}
- disabled={readOnly}
- />
- ) : (
- Empty
- )}
+
+ handleUpdateIssue({
+ start_date: val,
+ })
+ }
+ className="bg-custom-background-80 border-none"
+ maxDate={maxDate ?? undefined}
+ disabled={readOnly}
+ />
@@ -153,23 +148,18 @@ export const PeekOverviewIssueProperties: React.FC
= ({
Due date
- {issue.target_date ? (
-
- handleUpdateIssue({
- target_date: val,
- })
- }
- className="bg-custom-background-100"
- wrapperClassName="w-full"
- minDate={minDate ?? undefined}
- disabled={readOnly}
- />
- ) : (
- Empty
- )}
+
+ handleUpdateIssue({
+ target_date: val,
+ })
+ }
+ className="bg-custom-background-80 border-none"
+ minDate={minDate ?? undefined}
+ disabled={readOnly}
+ />
{/*
diff --git a/apps/app/components/issues/peek-overview/layout.tsx b/apps/app/components/issues/peek-overview/layout.tsx
index ce026e6a2..40737b6e8 100644
--- a/apps/app/components/issues/peek-overview/layout.tsx
+++ b/apps/app/components/issues/peek-overview/layout.tsx
@@ -2,6 +2,7 @@ import React, { useEffect, useState } from "react";
import { useRouter } from "next/router";
+import { mutate } from "swr";
// mobx
import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider";
@@ -10,9 +11,11 @@ import { Dialog, Transition } from "@headlessui/react";
// hooks
import useUser from "hooks/use-user";
// components
-import { FullScreenPeekView, SidePeekView } from "components/issues";
+import { DeleteIssueModal, FullScreenPeekView, SidePeekView } from "components/issues";
// types
import { IIssue } from "types";
+// fetch-keys
+import { PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
type Props = {
handleMutation: () => void;
@@ -28,6 +31,7 @@ export const IssuePeekOverview: React.FC
= observer(
const [isSidePeekOpen, setIsSidePeekOpen] = useState(false);
const [isModalPeekOpen, setIsModalPeekOpen] = useState(false);
const [peekOverviewMode, setPeekOverviewMode] = useState("side");
+ const [deleteIssueModal, setDeleteIssueModal] = useState(false);
const router = useRouter();
const { peekIssue } = router.query;
@@ -53,6 +57,7 @@ export const IssuePeekOverview: React.FC = observer(
if (!issue || !user) return;
await updateIssue(workspaceSlug, projectId, issue.id, formData, user);
+ mutate(PROJECT_ISSUES_ACTIVITY(issue.id));
handleMutation();
};
@@ -88,33 +93,38 @@ export const IssuePeekOverview: React.FC = observer(
return (
<>
+ setDeleteIssueModal(false)}
+ data={issue ? { ...issue } : null}
+ onSubmit={handleDeleteIssue}
+ user={user}
+ />
) : (
- Select co-worker’s email
+ Select co-worker
)}
diff --git a/apps/app/components/ui/dropdowns/custom-select.tsx b/apps/app/components/ui/dropdowns/custom-select.tsx
index 4e495a210..ae814dccb 100644
--- a/apps/app/components/ui/dropdowns/custom-select.tsx
+++ b/apps/app/components/ui/dropdowns/custom-select.tsx
@@ -41,7 +41,7 @@ const CustomSelect = ({
>
<>
{customButton ? (
- {customButton}
+ {customButton}
) : (
{
+ const [user, setUser] = useState();
+ const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved");
+ const [isLoading, setIsLoading] = useState("false");
+ const { setShowAlert } = useReloadConfirmations();
+ const [cookies, setCookies] = useState({});
+ const [issueDetail, setIssueDetail] = useState(null);
+ const router = useRouter();
+ const { editable } = router.query;
+ const {
+ handleSubmit,
+ watch,
+ setValue,
+ control,
+ formState: { errors },
+ } = useForm({
+ defaultValues: {
+ name: "",
+ description: "",
+ description_html: "",
+ },
+ });
+
+ const getCookies = () => {
+ const cookies = document.cookie.split(";");
+ const cookieObj: any = {};
+ cookies.forEach((cookie) => {
+ const cookieArr = cookie.split("=");
+ cookieObj[cookieArr[0].trim()] = cookieArr[1];
+ });
+
+ setCookies(cookieObj);
+ return cookieObj;
+ };
+
+ const getIssueDetail = async (cookiesData: any) => {
+ try {
+ setIsLoading("true");
+ const userData = await userService.currentUser();
+ setUser(userData);
+ const issueDetail = await issuesService.retrieve(
+ cookiesData.MOBILE_slug,
+ cookiesData.MOBILE_project_id,
+ cookiesData.MOBILE_issue_id
+ );
+ setIssueDetail(issueDetail);
+ setIsLoading("false");
+ setValue("description_html", issueDetail.description_html);
+ setValue("description", issueDetail.description);
+ } catch (e) {
+ setIsLoading("error");
+ console.log(e);
+ }
+ };
+ useEffect(() => {
+ const cookiesData = getCookies();
+
+ getIssueDetail(cookiesData);
+ }, []);
+
+ useEffect(() => {
+ if (isSubmitting === "submitted") {
+ setShowAlert(false);
+ setTimeout(async () => {
+ setIsSubmitting("saved");
+ }, 2000);
+ } else if (isSubmitting === "submitting") {
+ setShowAlert(true);
+ }
+ }, [isSubmitting, setShowAlert]);
+
+ const submitChanges = async (
+ formData: Partial,
+ workspaceSlug: string,
+ projectId: string,
+ issueId: string
+ ) => {
+ if (!workspaceSlug || !projectId || !issueId) return;
+
+ const payload: Partial = {
+ ...formData,
+ };
+
+ delete payload.blocker_issues;
+ delete payload.blocked_issues;
+ await issuesService
+ .patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload, user)
+ .catch((e) => {
+ console.log(e);
+ });
+ };
+
+ const handleDescriptionFormSubmit = useCallback(
+ async (formData: Partial) => {
+ if (!formData) return;
+
+ await submitChanges(
+ {
+ name: issueDetail?.name ?? "",
+ description: formData.description ?? "",
+ description_html: formData.description_html ?? "",
+ },
+ cookies.MOBILE_slug,
+ cookies.MOBILE_project_id,
+ cookies.MOBILE_issue_id
+ );
+ },
+ [submitChanges]
+ );
+
+ return isLoading === "error" ? (
+
+ ) : isLoading === "true" ? (
+
+
+
+ ) : (
+
+
(
+ {
+ setShowAlert(true);
+ setIsSubmitting("submitting");
+ onChange(description_html);
+ setValue("description", description);
+ handleSubmit(handleDescriptionFormSubmit)().finally(() => {
+ setIsSubmitting("submitted");
+ });
+ }}
+ />
+ )}
+ />
+
+ {isSubmitting === "submitting" ? "Saving..." : "Saved"}
+
+
+ );
+};
+
+const ErrorEncountered: NextPage = () => (
+
+
+
+
+
+
+
+
Oops! Something went wrong.
+
+
+
+
+);
+
+export default Editor;
diff --git a/apps/app/store/issues.ts b/apps/app/store/issues.ts
index 538c5e2a9..286d98534 100644
--- a/apps/app/store/issues.ts
+++ b/apps/app/store/issues.ts
@@ -116,11 +116,12 @@ class IssuesStore {
const originalIssue = { ...this.issues[issueId] };
// immediately update the issue in the store
- const updatedIssue = { ...originalIssue, ...issueForm };
+ const updatedIssue = { ...this.issues[issueId], ...issueForm };
+ if (updatedIssue.assignees_list) updatedIssue.assignees = updatedIssue.assignees_list;
try {
runInAction(() => {
- this.issues[issueId] = updatedIssue;
+ this.issues[issueId] = { ...updatedIssue };
});
// make a patch request to update the issue