mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
Merge branch 'develop' of github.com:makeplane/plane into feat/deploy-pages
This commit is contained in:
commit
a09e797de8
@ -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
|
||||
|
@ -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 = (
|
||||
|
@ -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,
|
||||
)
|
||||
|
||||
|
@ -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')},
|
||||
},
|
||||
),
|
||||
]
|
||||
|
@ -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')},
|
||||
),
|
||||
]
|
@ -293,7 +293,7 @@ class IssueComment(ProjectBaseModel):
|
||||
comment_json = models.JSONField(blank=True, default=dict)
|
||||
comment_html = models.TextField(blank=True, default="<p></p>")
|
||||
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,
|
||||
|
@ -73,9 +73,11 @@ export const ChartDraggable: React.FC<Props> = ({
|
||||
};
|
||||
|
||||
// handle block resize from the left end
|
||||
const handleBlockLeftResize = () => {
|
||||
const handleBlockLeftResize = (e: React.MouseEvent<HTMLDivElement, 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<Props> = ({
|
||||
};
|
||||
|
||||
// handle block resize from the right end
|
||||
const handleBlockRightResize = () => {
|
||||
const handleBlockRightResize = (e: React.MouseEvent<HTMLDivElement, 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<Props> = ({
|
||||
const handleBlockMove = (e: React.MouseEvent<HTMLDivElement, 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<Props> = ({
|
||||
<div
|
||||
id={`block-${block.id}`}
|
||||
ref={resizableRef}
|
||||
className="relative group cursor-pointer font-medium rounded shadow-sm h-full inline-flex items-center transition-all"
|
||||
className="relative group cursor-pointer font-medium h-full inline-flex items-center transition-all"
|
||||
style={{
|
||||
marginLeft: `${block.position?.marginLeft}px`,
|
||||
width: `${block.position?.width}px`,
|
||||
|
@ -33,10 +33,17 @@ type Props = {
|
||||
isOpen: boolean;
|
||||
handleClose: () => void;
|
||||
data: IIssue | null;
|
||||
onSubmit?: () => Promise<void>;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
};
|
||||
|
||||
export const DeleteIssueModal: React.FC<Props> = ({ isOpen, handleClose, data, user }) => {
|
||||
export const DeleteIssueModal: React.FC<Props> = ({
|
||||
isOpen,
|
||||
handleClose,
|
||||
data,
|
||||
onSubmit,
|
||||
user,
|
||||
}) => {
|
||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
@ -116,6 +123,8 @@ export const DeleteIssueModal: React.FC<Props> = ({ isOpen, handleClose, data, u
|
||||
else mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(data.project, params));
|
||||
}
|
||||
|
||||
if (onSubmit) onSubmit();
|
||||
|
||||
handleClose();
|
||||
setToastAlert({
|
||||
title: "Success",
|
||||
|
@ -103,7 +103,7 @@ export const PeekOverviewIssueProperties: React.FC<Props> = ({
|
||||
</div>
|
||||
<div className="w-3/4">
|
||||
<SidebarAssigneeSelect
|
||||
value={issue.assignees_list}
|
||||
value={issue.assignees}
|
||||
onChange={(val: string[]) => handleUpdateIssue({ assignees_list: val })}
|
||||
disabled={readOnly}
|
||||
/>
|
||||
@ -128,23 +128,18 @@ export const PeekOverviewIssueProperties: React.FC<Props> = ({
|
||||
<span className="flex-grow truncate">Start date</span>
|
||||
</div>
|
||||
<div>
|
||||
{issue.start_date ? (
|
||||
<CustomDatePicker
|
||||
placeholder="Start date"
|
||||
value={issue.start_date}
|
||||
onChange={(val) =>
|
||||
handleUpdateIssue({
|
||||
start_date: val,
|
||||
})
|
||||
}
|
||||
className="bg-custom-background-100"
|
||||
wrapperClassName="w-full"
|
||||
maxDate={maxDate ?? undefined}
|
||||
disabled={readOnly}
|
||||
/>
|
||||
) : (
|
||||
<span className="text-custom-text-200">Empty</span>
|
||||
)}
|
||||
<CustomDatePicker
|
||||
placeholder="Select start date"
|
||||
value={issue.start_date}
|
||||
onChange={(val) =>
|
||||
handleUpdateIssue({
|
||||
start_date: val,
|
||||
})
|
||||
}
|
||||
className="bg-custom-background-80 border-none"
|
||||
maxDate={maxDate ?? undefined}
|
||||
disabled={readOnly}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
@ -153,23 +148,18 @@ export const PeekOverviewIssueProperties: React.FC<Props> = ({
|
||||
<span className="flex-grow truncate">Due date</span>
|
||||
</div>
|
||||
<div>
|
||||
{issue.target_date ? (
|
||||
<CustomDatePicker
|
||||
placeholder="Due date"
|
||||
value={issue.target_date}
|
||||
onChange={(val) =>
|
||||
handleUpdateIssue({
|
||||
target_date: val,
|
||||
})
|
||||
}
|
||||
className="bg-custom-background-100"
|
||||
wrapperClassName="w-full"
|
||||
minDate={minDate ?? undefined}
|
||||
disabled={readOnly}
|
||||
/>
|
||||
) : (
|
||||
<span className="text-custom-text-200">Empty</span>
|
||||
)}
|
||||
<CustomDatePicker
|
||||
placeholder="Select due date"
|
||||
value={issue.target_date}
|
||||
onChange={(val) =>
|
||||
handleUpdateIssue({
|
||||
target_date: val,
|
||||
})
|
||||
}
|
||||
className="bg-custom-background-80 border-none"
|
||||
minDate={minDate ?? undefined}
|
||||
disabled={readOnly}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className="flex items-center gap-2 text-sm">
|
||||
|
@ -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<Props> = observer(
|
||||
const [isSidePeekOpen, setIsSidePeekOpen] = useState(false);
|
||||
const [isModalPeekOpen, setIsModalPeekOpen] = useState(false);
|
||||
const [peekOverviewMode, setPeekOverviewMode] = useState<TPeekOverviewModes>("side");
|
||||
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { peekIssue } = router.query;
|
||||
@ -53,6 +57,7 @@ export const IssuePeekOverview: React.FC<Props> = 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<Props> = observer(
|
||||
|
||||
return (
|
||||
<>
|
||||
<DeleteIssueModal
|
||||
isOpen={deleteIssueModal}
|
||||
handleClose={() => setDeleteIssueModal(false)}
|
||||
data={issue ? { ...issue } : null}
|
||||
onSubmit={handleDeleteIssue}
|
||||
user={user}
|
||||
/>
|
||||
<Transition.Root appear show={isSidePeekOpen} as={React.Fragment}>
|
||||
<Dialog as="div" className="relative z-20" onClose={handleClose}>
|
||||
<div className="fixed inset-0 z-20 overflow-y-auto">
|
||||
<div className="relative h-full w-full">
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="transition-transform duration-300"
|
||||
enterFrom="translate-x-full"
|
||||
enterTo="translate-x-0"
|
||||
leave="transition-transform duration-200"
|
||||
leaveFrom="translate-x-0"
|
||||
leaveTo="translate-x-full"
|
||||
>
|
||||
<Dialog.Panel className="absolute z-20 bg-custom-background-100 top-0 right-0 h-full w-1/2 shadow-custom-shadow-md">
|
||||
<SidePeekView
|
||||
handleClose={handleClose}
|
||||
handleDeleteIssue={handleDeleteIssue}
|
||||
handleUpdateIssue={handleUpdateIssue}
|
||||
issue={issue}
|
||||
mode={peekOverviewMode}
|
||||
readOnly={readOnly}
|
||||
setMode={(mode) => setPeekOverviewMode(mode)}
|
||||
workspaceSlug={workspaceSlug}
|
||||
/>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
<div className="fixed inset-0 z-20 h-full w-full overflow-y-auto">
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="transition-transform duration-300"
|
||||
enterFrom="translate-x-full"
|
||||
enterTo="translate-x-0"
|
||||
leave="transition-transform duration-200"
|
||||
leaveFrom="translate-x-0"
|
||||
leaveTo="translate-x-full"
|
||||
>
|
||||
<Dialog.Panel className="fixed z-20 bg-custom-background-100 top-0 right-0 h-full w-1/2 shadow-custom-shadow-md">
|
||||
<SidePeekView
|
||||
handleClose={handleClose}
|
||||
handleDeleteIssue={() => setDeleteIssueModal(true)}
|
||||
handleUpdateIssue={handleUpdateIssue}
|
||||
issue={issue}
|
||||
mode={peekOverviewMode}
|
||||
readOnly={readOnly}
|
||||
setMode={(mode) => setPeekOverviewMode(mode)}
|
||||
workspaceSlug={workspaceSlug}
|
||||
/>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
@ -131,49 +141,47 @@ export const IssuePeekOverview: React.FC<Props> = observer(
|
||||
>
|
||||
<div className="fixed inset-0 bg-custom-backdrop bg-opacity-50 transition-opacity" />
|
||||
</Transition.Child>
|
||||
<div className="fixed inset-0 z-20 overflow-y-auto">
|
||||
<div className="relative h-full w-full">
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
<div className="fixed inset-0 z-20 h-full w-full overflow-y-auto">
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Dialog.Panel
|
||||
className={`fixed z-20 bg-custom-background-100 top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 rounded-lg shadow-custom-shadow-xl transition-all duration-300 ${
|
||||
peekOverviewMode === "modal" ? "h-[70%] w-3/5" : "h-[95%] w-[95%]"
|
||||
}`}
|
||||
>
|
||||
<Dialog.Panel
|
||||
className={`absolute z-20 bg-custom-background-100 top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 rounded-lg shadow-custom-shadow-xl transition-all duration-300 ${
|
||||
peekOverviewMode === "modal" ? "h-[70%] w-3/5" : "h-[95%] w-[95%]"
|
||||
}`}
|
||||
>
|
||||
{peekOverviewMode === "modal" && (
|
||||
<SidePeekView
|
||||
handleClose={handleClose}
|
||||
handleDeleteIssue={handleDeleteIssue}
|
||||
handleUpdateIssue={handleUpdateIssue}
|
||||
issue={issue}
|
||||
mode={peekOverviewMode}
|
||||
readOnly={readOnly}
|
||||
setMode={(mode) => setPeekOverviewMode(mode)}
|
||||
workspaceSlug={workspaceSlug}
|
||||
/>
|
||||
)}
|
||||
{peekOverviewMode === "full" && (
|
||||
<FullScreenPeekView
|
||||
handleClose={handleClose}
|
||||
handleDeleteIssue={handleDeleteIssue}
|
||||
handleUpdateIssue={handleUpdateIssue}
|
||||
issue={issue}
|
||||
mode={peekOverviewMode}
|
||||
readOnly={readOnly}
|
||||
setMode={(mode) => setPeekOverviewMode(mode)}
|
||||
workspaceSlug={workspaceSlug}
|
||||
/>
|
||||
)}
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
{peekOverviewMode === "modal" && (
|
||||
<SidePeekView
|
||||
handleClose={handleClose}
|
||||
handleDeleteIssue={() => setDeleteIssueModal(true)}
|
||||
handleUpdateIssue={handleUpdateIssue}
|
||||
issue={issue}
|
||||
mode={peekOverviewMode}
|
||||
readOnly={readOnly}
|
||||
setMode={(mode) => setPeekOverviewMode(mode)}
|
||||
workspaceSlug={workspaceSlug}
|
||||
/>
|
||||
)}
|
||||
{peekOverviewMode === "full" && (
|
||||
<FullScreenPeekView
|
||||
handleClose={handleClose}
|
||||
handleDeleteIssue={() => setDeleteIssueModal(true)}
|
||||
handleUpdateIssue={handleUpdateIssue}
|
||||
issue={issue}
|
||||
mode={peekOverviewMode}
|
||||
readOnly={readOnly}
|
||||
setMode={(mode) => setPeekOverviewMode(mode)}
|
||||
workspaceSlug={workspaceSlug}
|
||||
/>
|
||||
)}
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
|
@ -51,7 +51,10 @@ export const SidebarAssigneeSelect: React.FC<Props> = ({ value, onChange, disabl
|
||||
<span className="text-custom-text-100 text-xs">{value.length} Assignees</span>
|
||||
</div>
|
||||
) : (
|
||||
<button type="button" className="bg-custom-background-80 px-2.5 py-0.5 text-xs rounded">
|
||||
<button
|
||||
type="button"
|
||||
className="bg-custom-background-80 px-2.5 py-0.5 text-xs rounded text-custom-text-200"
|
||||
>
|
||||
No assignees
|
||||
</button>
|
||||
)}
|
||||
|
@ -27,7 +27,7 @@ export const SidebarPrioritySelect: React.FC<Props> = ({ value, onChange, disabl
|
||||
? "border-yellow-500/20 bg-yellow-500/20 text-yellow-500"
|
||||
: value === "low"
|
||||
? "border-green-500/20 bg-green-500/20 text-green-500"
|
||||
: "bg-custom-background-80 border-custom-border-200"
|
||||
: "bg-custom-background-80 border-custom-border-200 text-custom-text-200"
|
||||
}`}
|
||||
>
|
||||
<span className="grid place-items-center -my-1">
|
||||
|
@ -218,7 +218,7 @@ const SendProjectInvitationModal: React.FC<Props> = (props) => {
|
||||
}
|
||||
</div>
|
||||
) : (
|
||||
<div>Select co-worker’s email</div>
|
||||
<div>Select co-worker</div>
|
||||
)}
|
||||
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
||||
</button>
|
||||
|
@ -41,7 +41,7 @@ const CustomSelect = ({
|
||||
>
|
||||
<>
|
||||
{customButton ? (
|
||||
<Listbox.Button as="div">{customButton}</Listbox.Button>
|
||||
<Listbox.Button as={React.Fragment}>{customButton}</Listbox.Button>
|
||||
) : (
|
||||
<Listbox.Button
|
||||
type="button"
|
||||
|
192
apps/app/pages/[workspaceSlug]/editor.tsx
Normal file
192
apps/app/pages/[workspaceSlug]/editor.tsx
Normal file
@ -0,0 +1,192 @@
|
||||
import { TipTapEditor } from "components/tiptap";
|
||||
import type { NextPage } from "next";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import issuesService from "services/issues.service";
|
||||
import { ICurrentUserResponse, IIssue } from "types";
|
||||
import useReloadConfirmations from "hooks/use-reload-confirmation";
|
||||
import { Spinner } from "components/ui";
|
||||
import Image404 from "public/404.svg";
|
||||
import DefaultLayout from "layouts/default-layout";
|
||||
import Image from "next/image";
|
||||
import userService from "services/user.service";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
const Editor: NextPage = () => {
|
||||
const [user, setUser] = useState<ICurrentUserResponse | undefined>();
|
||||
const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved");
|
||||
const [isLoading, setIsLoading] = useState("false");
|
||||
const { setShowAlert } = useReloadConfirmations();
|
||||
const [cookies, setCookies] = useState<any>({});
|
||||
const [issueDetail, setIssueDetail] = useState<IIssue | null>(null);
|
||||
const router = useRouter();
|
||||
const { editable } = router.query;
|
||||
const {
|
||||
handleSubmit,
|
||||
watch,
|
||||
setValue,
|
||||
control,
|
||||
formState: { errors },
|
||||
} = useForm<IIssue>({
|
||||
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<IIssue>,
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string
|
||||
) => {
|
||||
if (!workspaceSlug || !projectId || !issueId) return;
|
||||
|
||||
const payload: Partial<IIssue> = {
|
||||
...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<IIssue>) => {
|
||||
if (!formData) return;
|
||||
|
||||
await submitChanges(
|
||||
{
|
||||
name: issueDetail?.name ?? "",
|
||||
description: formData.description ?? "",
|
||||
description_html: formData.description_html ?? "<p></p>",
|
||||
},
|
||||
cookies.MOBILE_slug,
|
||||
cookies.MOBILE_project_id,
|
||||
cookies.MOBILE_issue_id
|
||||
);
|
||||
},
|
||||
[submitChanges]
|
||||
);
|
||||
|
||||
return isLoading === "error" ? (
|
||||
<ErrorEncountered />
|
||||
) : isLoading === "true" ? (
|
||||
<div className="grid place-items-center h-screen w-full">
|
||||
<Spinner />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex blur-none shadow-none backdrop:backdrop-blur-none justify-center items-center">
|
||||
<Controller
|
||||
name="description_html"
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<TipTapEditor
|
||||
borderOnFocus={false}
|
||||
value={
|
||||
!value ||
|
||||
value === "" ||
|
||||
(typeof value === "object" && Object.keys(value).length === 0)
|
||||
? watch("description_html")
|
||||
: value
|
||||
}
|
||||
editable={editable === "true"}
|
||||
noBorder={true}
|
||||
workspaceSlug={cookies.MOBILE_slug ?? ""}
|
||||
debouncedUpdatesEnabled={true}
|
||||
setShouldShowAlert={setShowAlert}
|
||||
setIsSubmitting={setIsSubmitting}
|
||||
customClassName="min-h-[150px] shadow-sm"
|
||||
editorContentCustomClassNames="pb-9"
|
||||
onChange={(description: Object, description_html: string) => {
|
||||
setShowAlert(true);
|
||||
setIsSubmitting("submitting");
|
||||
onChange(description_html);
|
||||
setValue("description", description);
|
||||
handleSubmit(handleDescriptionFormSubmit)().finally(() => {
|
||||
setIsSubmitting("submitted");
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
className={`absolute right-5 bottom-5 text-xs text-custom-text-200 border border-custom-border-400 rounded-xl w-[6.5rem] py-1 z-10 flex items-center justify-center ${
|
||||
isSubmitting === "saved" ? "fadeOut" : "fadeIn"
|
||||
}`}
|
||||
>
|
||||
{isSubmitting === "submitting" ? "Saving..." : "Saved"}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ErrorEncountered: NextPage = () => (
|
||||
<DefaultLayout>
|
||||
<div className="grid max-h-fit place-items-center p-4">
|
||||
<div className="space-y-8 text-center">
|
||||
<div className="relative mx-auto h-40 w-40 lg:h-40 lg:w-40">
|
||||
<Image src={Image404} layout="fill" alt="404- Page not found" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-lg font-semibold">Oops! Something went wrong.</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DefaultLayout>
|
||||
);
|
||||
|
||||
export default Editor;
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user