mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
[WEB-999] chore: updated UI improvements and workflow updates in the project inbox (#4180)
* chore: snoozed filter in the issue inbox filter * chore: navigating to the next or previous issue when we accept, decline, or duplicate the issue in inbox * chore: Implemented state, label, assignee and target_date in the inbox issue description and Implemented issue edit confirmation once we click accept the inbox issue * chore: removed logs * chore: inbox issue create response * chore: update inbox issue response * chore: updated inbox issue accept workflow and added issue properties in inbox issue create modal * chore: resolved build errors and upgraded lucide react * chore: updated inbox issue store hook * chore: code cleanup and removed validation for inbox description * fix: renamed the variable isLoading to loader in project-inbox store * fix: updated set function for issue property update --------- Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
This commit is contained in:
parent
a44a032683
commit
20b0edeaa6
@ -24,6 +24,7 @@ from plane.db.models import (
|
||||
State,
|
||||
IssueLink,
|
||||
IssueAttachment,
|
||||
Project,
|
||||
ProjectMember,
|
||||
)
|
||||
from plane.app.serializers import (
|
||||
@ -239,41 +240,75 @@ class InboxIssueViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
# create an issue
|
||||
issue = Issue.objects.create(
|
||||
name=request.data.get("issue", {}).get("name"),
|
||||
description=request.data.get("issue", {}).get("description", {}),
|
||||
description_html=request.data.get("issue", {}).get(
|
||||
"description_html", "<p></p>"
|
||||
),
|
||||
priority=request.data.get("issue", {}).get("priority", "low"),
|
||||
project_id=project_id,
|
||||
state=state,
|
||||
project = Project.objects.get(pk=project_id)
|
||||
serializer = IssueCreateSerializer(
|
||||
data=request.data.get("issue"),
|
||||
context={
|
||||
"project_id": project_id,
|
||||
"workspace_id": project.workspace_id,
|
||||
"default_assignee_id": project.default_assignee_id,
|
||||
},
|
||||
)
|
||||
|
||||
# Create an Issue Activity
|
||||
issue_activity.delay(
|
||||
type="issue.activity.created",
|
||||
requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=str(issue.id),
|
||||
project_id=str(project_id),
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
inbox_id = Inbox.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
).first()
|
||||
# create an inbox issue
|
||||
inbox_issue = InboxIssue.objects.create(
|
||||
inbox_id=inbox_id.id,
|
||||
project_id=project_id,
|
||||
issue=issue,
|
||||
source=request.data.get("source", "in-app"),
|
||||
)
|
||||
serializer = InboxIssueDetailSerializer(inbox_issue)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
# Create an Issue Activity
|
||||
issue_activity.delay(
|
||||
type="issue.activity.created",
|
||||
requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=str(serializer.data["id"]),
|
||||
project_id=str(project_id),
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
inbox_id = Inbox.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
).first()
|
||||
# create an inbox issue
|
||||
inbox_issue = InboxIssue.objects.create(
|
||||
inbox_id=inbox_id.id,
|
||||
project_id=project_id,
|
||||
issue_id=serializer.data["id"],
|
||||
source=request.data.get("source", "in-app"),
|
||||
)
|
||||
inbox_issue = (
|
||||
InboxIssue.objects.select_related("issue")
|
||||
.prefetch_related(
|
||||
"issue__labels",
|
||||
"issue__assignees",
|
||||
)
|
||||
.annotate(
|
||||
label_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"issue__labels__id",
|
||||
distinct=True,
|
||||
filter=~Q(issue__labels__id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
assignee_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"issue__assignees__id",
|
||||
distinct=True,
|
||||
filter=~Q(issue__assignees__id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
)
|
||||
.get(
|
||||
inbox_id=inbox_id.id,
|
||||
issue_id=serializer.data["id"],
|
||||
project_id=project_id,
|
||||
)
|
||||
)
|
||||
serializer = InboxIssueDetailSerializer(inbox_issue)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
else:
|
||||
return Response(
|
||||
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
def partial_update(self, request, slug, project_id, issue_id):
|
||||
inbox_id = Inbox.objects.filter(
|
||||
@ -395,6 +430,42 @@ class InboxIssueViewSet(BaseViewSet):
|
||||
issue.state = state
|
||||
issue.save()
|
||||
|
||||
inbox_issue = (
|
||||
InboxIssue.objects.select_related("issue")
|
||||
.prefetch_related(
|
||||
"issue__labels",
|
||||
"issue__assignees",
|
||||
)
|
||||
.annotate(
|
||||
label_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"issue__labels__id",
|
||||
distinct=True,
|
||||
filter=~Q(issue__labels__id__isnull=True),
|
||||
),
|
||||
Value(
|
||||
[],
|
||||
output_field=ArrayField(UUIDField()),
|
||||
),
|
||||
),
|
||||
assignee_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"issue__assignees__id",
|
||||
distinct=True,
|
||||
filter=~Q(issue__assignees__id__isnull=True),
|
||||
),
|
||||
Value(
|
||||
[],
|
||||
output_field=ArrayField(UUIDField()),
|
||||
),
|
||||
),
|
||||
)
|
||||
.get(
|
||||
inbox_id=inbox_id.id,
|
||||
issue_id=serializer.data["id"],
|
||||
project_id=project_id,
|
||||
)
|
||||
)
|
||||
serializer = InboxIssueDetailSerializer(inbox_issue).data
|
||||
return Response(serializer, status=status.HTTP_200_OK)
|
||||
return Response(
|
||||
|
@ -6,7 +6,7 @@ import { Plus, RefreshCcw } from "lucide-react";
|
||||
import { Breadcrumbs, Button, LayersIcon } from "@plane/ui";
|
||||
// components
|
||||
import { BreadcrumbLink } from "@/components/common";
|
||||
import { CreateInboxIssueModal } from "@/components/inbox";
|
||||
import { InboxIssueCreateEditModalRoot } from "@/components/inbox";
|
||||
import { ProjectLogo } from "@/components/project";
|
||||
// hooks
|
||||
import { useProject, useProjectInbox } from "@/hooks/store";
|
||||
@ -16,10 +16,10 @@ export const ProjectInboxHeader: FC = observer(() => {
|
||||
const [createIssueModal, setCreateIssueModal] = useState(false);
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
// store hooks
|
||||
const { currentProjectDetails } = useProject();
|
||||
const { isLoading } = useProjectInbox();
|
||||
const { loader } = useProjectInbox();
|
||||
|
||||
return (
|
||||
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 bg-custom-sidebar-background-100 p-4">
|
||||
@ -51,7 +51,7 @@ export const ProjectInboxHeader: FC = observer(() => {
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
|
||||
{isLoading === "pagination-loading" && (
|
||||
{loader === "pagination-loading" && (
|
||||
<div className="flex items-center gap-1.5 text-custom-text-300">
|
||||
<RefreshCcw className="h-3.5 w-3.5 animate-spin" />
|
||||
<p className="text-sm">Syncing...</p>
|
||||
@ -60,9 +60,16 @@ export const ProjectInboxHeader: FC = observer(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{currentProjectDetails?.inbox_view && (
|
||||
{currentProjectDetails?.inbox_view && workspaceSlug && projectId && (
|
||||
<div className="flex items-center gap-2">
|
||||
<CreateInboxIssueModal isOpen={createIssueModal} onClose={() => setCreateIssueModal(false)} />
|
||||
<InboxIssueCreateEditModalRoot
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
modalState={createIssueModal}
|
||||
handleModalClose={() => setCreateIssueModal(false)}
|
||||
issue={undefined}
|
||||
/>
|
||||
|
||||
<Button variant="primary" prependIcon={<Plus />} size="sm" onClick={() => setCreateIssueModal(true)}>
|
||||
Add Issue
|
||||
</Button>
|
||||
|
@ -1,13 +1,23 @@
|
||||
import { FC, useCallback, useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { ChevronDown, ChevronUp, Clock, ExternalLink, FileStack, Link, Trash2 } from "lucide-react";
|
||||
import {
|
||||
CircleCheck,
|
||||
CircleX,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Clock,
|
||||
ExternalLink,
|
||||
FileStack,
|
||||
Link,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { Button, ControlLink, CustomMenu, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import {
|
||||
AcceptIssueModal,
|
||||
DeclineIssueModal,
|
||||
DeleteInboxIssueModal,
|
||||
InboxIssueCreateEditModalRoot,
|
||||
InboxIssueSnoozeModal,
|
||||
InboxIssueStatus,
|
||||
SelectDuplicateInboxIssueModal,
|
||||
@ -39,7 +49,7 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
|
||||
const [declineIssueModal, setDeclineIssueModal] = useState(false);
|
||||
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
|
||||
// store
|
||||
const { deleteInboxIssue, inboxIssuesArray } = useProjectInbox();
|
||||
const { currentTab, deleteInboxIssue, inboxIssuesArray } = useProjectInbox();
|
||||
const {
|
||||
currentUser,
|
||||
membership: { currentProjectRole },
|
||||
@ -60,25 +70,50 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
|
||||
|
||||
const issueLink = `${workspaceSlug}/projects/${issue?.project_id}/issues/${currentInboxIssueId}`;
|
||||
|
||||
const redirectIssue = (): string | undefined => {
|
||||
let nextOrPreviousIssueId: string | undefined = undefined;
|
||||
const currentIssueIndex = inboxIssuesArray.findIndex((i) => i.issue.id === currentInboxIssueId);
|
||||
if (inboxIssuesArray[currentIssueIndex + 1])
|
||||
nextOrPreviousIssueId = inboxIssuesArray[currentIssueIndex + 1].issue.id;
|
||||
else if (inboxIssuesArray[currentIssueIndex - 1])
|
||||
nextOrPreviousIssueId = inboxIssuesArray[currentIssueIndex - 1].issue.id;
|
||||
else nextOrPreviousIssueId = undefined;
|
||||
return nextOrPreviousIssueId;
|
||||
};
|
||||
|
||||
const handleRedirection = (nextOrPreviousIssueId: string | undefined) => {
|
||||
if (nextOrPreviousIssueId)
|
||||
router.push(
|
||||
`/${workspaceSlug}/projects/${projectId}/inbox?currentTab=${currentTab}&inboxIssueId=${nextOrPreviousIssueId}`
|
||||
);
|
||||
else router.push(`/${workspaceSlug}/projects/${projectId}/inbox?currentTab=${currentTab}`);
|
||||
};
|
||||
|
||||
const handleInboxIssueAccept = async () => {
|
||||
const nextOrPreviousIssueId = redirectIssue();
|
||||
await inboxIssue?.updateInboxIssueStatus(EInboxIssueStatus.ACCEPTED);
|
||||
setAcceptIssueModal(false);
|
||||
handleRedirection(nextOrPreviousIssueId);
|
||||
};
|
||||
|
||||
const handleInboxIssueDecline = async () => {
|
||||
const nextOrPreviousIssueId = redirectIssue();
|
||||
await inboxIssue?.updateInboxIssueStatus(EInboxIssueStatus.DECLINED);
|
||||
setDeclineIssueModal(false);
|
||||
handleRedirection(nextOrPreviousIssueId);
|
||||
};
|
||||
|
||||
const handleInboxSIssueSnooze = async (date: Date) => {
|
||||
const nextOrPreviousIssueId = redirectIssue();
|
||||
await inboxIssue?.updateInboxIssueSnoozeTill(date);
|
||||
setIsSnoozeDateModalOpen(false);
|
||||
handleRedirection(nextOrPreviousIssueId);
|
||||
};
|
||||
|
||||
const handleInboxIssueDuplicate = async (issueId: string) => {
|
||||
await inboxIssue?.updateInboxIssueDuplicateTo(issueId);
|
||||
};
|
||||
|
||||
const handleInboxSIssueSnooze = async (date: Date) => {
|
||||
await inboxIssue?.updateInboxIssueSnoozeTill(date);
|
||||
setIsSnoozeDateModalOpen(false);
|
||||
};
|
||||
|
||||
const handleInboxIssueDelete = async () => {
|
||||
if (!inboxIssue || !currentInboxIssueId) return;
|
||||
await deleteInboxIssue(workspaceSlug, projectId, currentInboxIssueId).finally(() => {
|
||||
@ -143,10 +178,12 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
|
||||
onSubmit={handleInboxIssueDuplicate}
|
||||
/>
|
||||
|
||||
<AcceptIssueModal
|
||||
data={inboxIssue?.issue}
|
||||
isOpen={acceptIssueModal}
|
||||
onClose={() => setAcceptIssueModal(false)}
|
||||
<InboxIssueCreateEditModalRoot
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
modalState={acceptIssueModal}
|
||||
handleModalClose={() => setAcceptIssueModal(false)}
|
||||
issue={inboxIssue?.issue}
|
||||
onSubmit={handleInboxIssueAccept}
|
||||
/>
|
||||
|
||||
@ -156,14 +193,12 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
|
||||
onClose={() => setDeclineIssueModal(false)}
|
||||
onSubmit={handleInboxIssueDecline}
|
||||
/>
|
||||
|
||||
<DeleteInboxIssueModal
|
||||
data={inboxIssue?.issue}
|
||||
isOpen={deleteIssueModal}
|
||||
onClose={() => setDeleteIssueModal(false)}
|
||||
onSubmit={handleInboxIssueDelete}
|
||||
/>
|
||||
|
||||
<InboxIssueSnoozeModal
|
||||
isOpen={isSnoozeDateModalOpen}
|
||||
handleClose={() => setIsSnoozeDateModalOpen(false)}
|
||||
@ -206,7 +241,13 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{canMarkAsAccepted && (
|
||||
<div className="flex-shrink-0">
|
||||
<Button variant="neutral-primary" size="sm" onClick={() => setAcceptIssueModal(true)}>
|
||||
<Button
|
||||
variant="neutral-primary"
|
||||
size="sm"
|
||||
prependIcon={<CircleCheck className="w-3 h-3" />}
|
||||
className="text-green-500 border-0.5 border-green-500 bg-green-500/20 focus:bg-green-500/20 focus:text-green-500 hover:bg-green-500/40 bg-opacity-20"
|
||||
onClick={() => setAcceptIssueModal(true)}
|
||||
>
|
||||
Accept
|
||||
</Button>
|
||||
</div>
|
||||
@ -214,7 +255,13 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
|
||||
|
||||
{canMarkAsDeclined && (
|
||||
<div className="flex-shrink-0">
|
||||
<Button variant="neutral-primary" size="sm" onClick={() => setDeclineIssueModal(true)}>
|
||||
<Button
|
||||
variant="neutral-primary"
|
||||
size="sm"
|
||||
prependIcon={<CircleX className="w-3 h-3" />}
|
||||
className="text-red-500 border-0.5 border-red-500 bg-red-500/20 focus:bg-red-500/20 focus:text-red-500 hover:bg-red-500/40 bg-opacity-20"
|
||||
onClick={() => setDeclineIssueModal(true)}
|
||||
>
|
||||
Decline
|
||||
</Button>
|
||||
</div>
|
||||
|
@ -21,7 +21,7 @@ type Props = {
|
||||
duplicateIssueDetails: TInboxDuplicateIssueDetails | undefined;
|
||||
};
|
||||
|
||||
export const InboxIssueProperties: React.FC<Props> = observer((props) => {
|
||||
export const InboxIssueContentProperties: React.FC<Props> = observer((props) => {
|
||||
const { workspaceSlug, projectId, issue, issueOperations, isEditable, duplicateIssueDetails } = props;
|
||||
|
||||
const router = useRouter();
|
||||
|
@ -2,9 +2,9 @@ import { Dispatch, SetStateAction, useEffect, useMemo } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { TIssue } from "@plane/types";
|
||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { Loader, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { InboxIssueProperties } from "@/components/inbox/content";
|
||||
import { InboxIssueContentProperties } from "@/components/inbox/content";
|
||||
import {
|
||||
IssueDescriptionInput,
|
||||
IssueTitleInput,
|
||||
@ -13,7 +13,7 @@ import {
|
||||
TIssueOperations,
|
||||
} from "@/components/issues";
|
||||
// hooks
|
||||
import { useEventTracker, useUser } from "@/hooks/store";
|
||||
import { useEventTracker, useProjectInbox, useUser } from "@/hooks/store";
|
||||
import useReloadConfirmations from "@/hooks/use-reload-confirmation";
|
||||
// store types
|
||||
import { IInboxIssueStore } from "@/store/inbox/inbox-issue.store";
|
||||
@ -36,6 +36,7 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
|
||||
const { currentUser } = useUser();
|
||||
const { setShowAlert } = useReloadConfirmations(isSubmitting === "submitting");
|
||||
const { captureIssueEvent } = useEventTracker();
|
||||
const { loader } = useProjectInbox();
|
||||
|
||||
useEffect(() => {
|
||||
if (isSubmitting === "submitted") {
|
||||
@ -126,16 +127,22 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
|
||||
value={issue.name}
|
||||
/>
|
||||
|
||||
<IssueDescriptionInput
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={issue.project_id}
|
||||
issueId={issue.id}
|
||||
swrIssueDescription={swrIssueDescription}
|
||||
initialValue={issue.description_html ?? "<p></p>"}
|
||||
disabled={!isEditable}
|
||||
issueOperations={issueOperations}
|
||||
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||
/>
|
||||
{loader === "issue-loading" ? (
|
||||
<Loader className="min-h-[6rem] rounded-md border border-custom-border-200">
|
||||
<Loader.Item width="100%" height="140px" />
|
||||
</Loader>
|
||||
) : (
|
||||
<IssueDescriptionInput
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={issue.project_id}
|
||||
issueId={issue.id}
|
||||
swrIssueDescription={swrIssueDescription}
|
||||
initialValue={issue.description_html ?? "<p></p>"}
|
||||
disabled={!isEditable}
|
||||
issueOperations={issueOperations}
|
||||
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentUser && (
|
||||
<IssueReaction
|
||||
@ -147,7 +154,7 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<InboxIssueProperties
|
||||
<InboxIssueContentProperties
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issue={issue}
|
||||
|
147
web/components/inbox/modals/create-edit-modal/create-root.tsx
Normal file
147
web/components/inbox/modals/create-edit-modal/create-root.tsx
Normal file
@ -0,0 +1,147 @@
|
||||
import { FC, useCallback, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { EditorRefApi } from "@plane/rich-text-editor";
|
||||
import { TIssue } from "@plane/types";
|
||||
import { Button, ToggleSwitch, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import {
|
||||
InboxIssueTitle,
|
||||
InboxIssueDescription,
|
||||
InboxIssueProperties,
|
||||
} from "@/components/inbox/modals/create-edit-modal";
|
||||
// constants
|
||||
import { ISSUE_CREATED } from "@/constants/event-tracker";
|
||||
// helpers
|
||||
import { renderFormattedPayloadDate } from "@/helpers/date-time.helper";
|
||||
// hooks
|
||||
import { useEventTracker, useProjectInbox, useWorkspace } from "@/hooks/store";
|
||||
|
||||
type TInboxIssueCreateRoot = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
handleModalClose: () => void;
|
||||
};
|
||||
|
||||
export const defaultIssueData: Partial<TIssue> = {
|
||||
id: undefined,
|
||||
name: "",
|
||||
description_html: "",
|
||||
priority: "none",
|
||||
state_id: "",
|
||||
label_ids: [],
|
||||
assignee_ids: [],
|
||||
start_date: renderFormattedPayloadDate(new Date()),
|
||||
target_date: "",
|
||||
};
|
||||
|
||||
export const InboxIssueCreateRoot: FC<TInboxIssueCreateRoot> = observer((props) => {
|
||||
const { workspaceSlug, projectId, handleModalClose } = props;
|
||||
const router = useRouter();
|
||||
// refs
|
||||
const descriptionEditorRef = useRef<EditorRefApi>(null);
|
||||
// hooks
|
||||
const { captureIssueEvent } = useEventTracker();
|
||||
const { createInboxIssue } = useProjectInbox();
|
||||
const { getWorkspaceBySlug } = useWorkspace();
|
||||
const workspaceId = getWorkspaceBySlug(workspaceSlug)?.id;
|
||||
// states
|
||||
const [createMore, setCreateMore] = useState<boolean>(false);
|
||||
const [formSubmitting, setFormSubmitting] = useState(false);
|
||||
const [formData, setFormData] = useState<Partial<TIssue>>(defaultIssueData);
|
||||
const handleFormData = useCallback(
|
||||
<T extends keyof Partial<TIssue>>(issueKey: T, issueValue: Partial<TIssue>[T]) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
[issueKey]: issueValue,
|
||||
});
|
||||
},
|
||||
[formData]
|
||||
);
|
||||
|
||||
const handleFormSubmit = async () => {
|
||||
const payload: Partial<TIssue> = {
|
||||
name: formData.name || "",
|
||||
description_html: formData.description_html || "<p></p>",
|
||||
priority: formData.priority || "none",
|
||||
state_id: formData.state_id || "",
|
||||
label_ids: formData.label_ids || [],
|
||||
assignee_ids: formData.assignee_ids || [],
|
||||
target_date: formData.target_date || null,
|
||||
};
|
||||
setFormSubmitting(true);
|
||||
|
||||
await createInboxIssue(workspaceSlug, projectId, payload)
|
||||
.then((res) => {
|
||||
if (!createMore) {
|
||||
router.push(`/${workspaceSlug}/projects/${projectId}/inbox/?currentTab=open&inboxIssueId=${res?.issue?.id}`);
|
||||
handleModalClose();
|
||||
} else {
|
||||
descriptionEditorRef?.current?.clearEditor();
|
||||
setFormData(defaultIssueData);
|
||||
}
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_CREATED,
|
||||
payload: {
|
||||
...formData,
|
||||
state: "SUCCESS",
|
||||
element: "Inbox page",
|
||||
},
|
||||
path: router.pathname,
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: `${TOAST_TYPE.SUCCESS}!`,
|
||||
message: "Issue created successfully.",
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_CREATED,
|
||||
payload: {
|
||||
...formData,
|
||||
state: "FAILED",
|
||||
element: "Inbox page",
|
||||
},
|
||||
path: router.pathname,
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: `${TOAST_TYPE.ERROR}!`,
|
||||
message: "Some error occurred. Please try again.",
|
||||
});
|
||||
});
|
||||
setFormSubmitting(false);
|
||||
};
|
||||
|
||||
if (!workspaceSlug || !projectId || !workspaceId) return <></>;
|
||||
return (
|
||||
<div className="relative space-y-4">
|
||||
<InboxIssueTitle data={formData} handleData={handleFormData} />
|
||||
<InboxIssueDescription
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
workspaceId={workspaceId}
|
||||
data={formData}
|
||||
handleData={handleFormData}
|
||||
editorRef={descriptionEditorRef}
|
||||
/>
|
||||
<InboxIssueProperties projectId={projectId} data={formData} handleData={handleFormData} />
|
||||
<div className="relative flex justify-between items-center gap-3">
|
||||
<div className="flex cursor-pointer items-center gap-1" onClick={() => setCreateMore((prevData) => !prevData)}>
|
||||
<span className="text-xs">Create more</span>
|
||||
<ToggleSwitch value={createMore} onChange={() => {}} size="md" />
|
||||
</div>
|
||||
<div className="relative flex items-center gap-3">
|
||||
<Button variant="neutral-primary" size="sm" type="button" onClick={handleModalClose}>
|
||||
Discard
|
||||
</Button>
|
||||
<Button variant="primary" size="sm" type="button" loading={formSubmitting} onClick={handleFormSubmit}>
|
||||
{formSubmitting ? "Adding Issue..." : "Add Issue"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
147
web/components/inbox/modals/create-edit-modal/edit-root.tsx
Normal file
147
web/components/inbox/modals/create-edit-modal/edit-root.tsx
Normal file
@ -0,0 +1,147 @@
|
||||
import { FC, useCallback, useEffect, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { EditorRefApi } from "@plane/rich-text-editor";
|
||||
import { TIssue } from "@plane/types";
|
||||
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import {
|
||||
InboxIssueTitle,
|
||||
InboxIssueDescription,
|
||||
InboxIssueProperties,
|
||||
} from "@/components/inbox/modals/create-edit-modal";
|
||||
// constants
|
||||
import { ISSUE_UPDATED } from "@/constants/event-tracker";
|
||||
// helpers
|
||||
import { renderFormattedPayloadDate } from "@/helpers/date-time.helper";
|
||||
// hooks
|
||||
import { useEventTracker, useInboxIssues, useWorkspace } from "@/hooks/store";
|
||||
|
||||
type TInboxIssueEditRoot = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
issueId: string;
|
||||
issue: Partial<TIssue>;
|
||||
handleModalClose: () => void;
|
||||
onSubmit?: () => void;
|
||||
};
|
||||
|
||||
export const InboxIssueEditRoot: FC<TInboxIssueEditRoot> = observer((props) => {
|
||||
const { workspaceSlug, projectId, issueId, issue, handleModalClose, onSubmit } = props;
|
||||
const router = useRouter();
|
||||
// refs
|
||||
const descriptionEditorRef = useRef<EditorRefApi>(null);
|
||||
// hooks
|
||||
const { captureIssueEvent } = useEventTracker();
|
||||
const { updateProjectIssue } = useInboxIssues(issueId);
|
||||
const { getWorkspaceBySlug } = useWorkspace();
|
||||
const workspaceId = getWorkspaceBySlug(workspaceSlug)?.id;
|
||||
// states
|
||||
const [formSubmitting, setFormSubmitting] = useState(false);
|
||||
const [formData, setFormData] = useState<Partial<TIssue> | undefined>(undefined);
|
||||
const handleFormData = useCallback(
|
||||
<T extends keyof Partial<TIssue>>(issueKey: T, issueValue: Partial<TIssue>[T]) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
[issueKey]: issueValue,
|
||||
});
|
||||
},
|
||||
[formData]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (formData?.id != issue?.id)
|
||||
setFormData({
|
||||
id: issue?.id || undefined,
|
||||
name: issue?.name ?? "",
|
||||
description_html: issue?.description_html ?? "<p></p>",
|
||||
priority: issue?.priority ?? "none",
|
||||
state_id: issue?.state_id ?? "",
|
||||
label_ids: issue?.label_ids ?? [],
|
||||
assignee_ids: issue?.assignee_ids ?? [],
|
||||
start_date: renderFormattedPayloadDate(issue?.start_date) ?? "",
|
||||
target_date: renderFormattedPayloadDate(issue?.target_date) ?? "",
|
||||
});
|
||||
}, [issue, formData]);
|
||||
|
||||
const handleFormSubmit = async () => {
|
||||
const payload: Partial<TIssue> = {
|
||||
name: formData?.name || "",
|
||||
description_html: formData?.description_html || "<p></p>",
|
||||
priority: formData?.priority || "none",
|
||||
state_id: formData?.state_id || "",
|
||||
label_ids: formData?.label_ids || [],
|
||||
assignee_ids: formData?.assignee_ids || [],
|
||||
start_date: formData?.start_date || undefined,
|
||||
target_date: formData?.target_date || undefined,
|
||||
cycle_id: formData?.cycle_id || "",
|
||||
module_ids: formData?.module_ids || [],
|
||||
estimate_point: formData?.estimate_point || undefined,
|
||||
parent_id: formData?.parent_id || "",
|
||||
};
|
||||
setFormSubmitting(true);
|
||||
|
||||
onSubmit && (await onSubmit());
|
||||
await updateProjectIssue(payload)
|
||||
.then(async () => {
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_UPDATED,
|
||||
payload: {
|
||||
...formData,
|
||||
state: "SUCCESS",
|
||||
element: "Inbox page",
|
||||
},
|
||||
path: router.pathname,
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: `${TOAST_TYPE.SUCCESS}!`,
|
||||
message: "Issue created successfully.",
|
||||
});
|
||||
descriptionEditorRef?.current?.clearEditor();
|
||||
handleModalClose();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_UPDATED,
|
||||
payload: {
|
||||
...formData,
|
||||
state: "FAILED",
|
||||
element: "Inbox page",
|
||||
},
|
||||
path: router.pathname,
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: `${TOAST_TYPE.ERROR}!`,
|
||||
message: "Some error occurred. Please try again.",
|
||||
});
|
||||
});
|
||||
setFormSubmitting(false);
|
||||
};
|
||||
|
||||
if (!workspaceSlug || !projectId || !workspaceId || !formData) return <></>;
|
||||
return (
|
||||
<div className="relative space-y-4">
|
||||
<InboxIssueTitle data={formData} handleData={handleFormData} />
|
||||
<InboxIssueDescription
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
workspaceId={workspaceId}
|
||||
data={formData}
|
||||
handleData={handleFormData}
|
||||
editorRef={descriptionEditorRef}
|
||||
/>
|
||||
<InboxIssueProperties projectId={projectId} data={formData} handleData={handleFormData} isVisible />
|
||||
<div className="relative flex justify-end items-center gap-3">
|
||||
<Button variant="neutral-primary" size="sm" type="button" onClick={handleModalClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" size="sm" type="button" loading={formSubmitting} onClick={handleFormSubmit}>
|
||||
{formSubmitting ? "Adding..." : "Add to project"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
6
web/components/inbox/modals/create-edit-modal/index.ts
Normal file
6
web/components/inbox/modals/create-edit-modal/index.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export * from "./modal";
|
||||
export * from "./create-root";
|
||||
export * from "./edit-root";
|
||||
export * from "./issue-title";
|
||||
export * from "./issue-description";
|
||||
export * from "./issue-properties";
|
@ -0,0 +1,47 @@
|
||||
import { FC, RefObject } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { EditorRefApi } from "@plane/rich-text-editor";
|
||||
import { TIssue } from "@plane/types";
|
||||
import { Loader } from "@plane/ui";
|
||||
// components
|
||||
import { RichTextEditor } from "@/components/editor/rich-text-editor/rich-text-editor";
|
||||
// hooks
|
||||
import { useProjectInbox } from "@/hooks/store";
|
||||
|
||||
type TInboxIssueDescription = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
workspaceId: string;
|
||||
data: Partial<TIssue>;
|
||||
handleData: (issueKey: keyof Partial<TIssue>, issueValue: Partial<TIssue>[keyof Partial<TIssue>]) => void;
|
||||
editorRef: RefObject<EditorRefApi>;
|
||||
};
|
||||
|
||||
// TODO: have to implement GPT Assistance
|
||||
export const InboxIssueDescription: FC<TInboxIssueDescription> = observer((props) => {
|
||||
const { workspaceSlug, projectId, workspaceId, data, handleData, editorRef } = props;
|
||||
// hooks
|
||||
const { loader } = useProjectInbox();
|
||||
|
||||
if (loader === "issue-loading")
|
||||
return (
|
||||
<Loader className="min-h-[6rem] rounded-md border border-custom-border-200">
|
||||
<Loader.Item width="100%" height="140px" />
|
||||
</Loader>
|
||||
);
|
||||
return (
|
||||
<div className="relative">
|
||||
<RichTextEditor
|
||||
initialValue={!data?.description_html || data?.description_html === "" ? "<p></p>" : data?.description_html}
|
||||
ref={editorRef}
|
||||
workspaceSlug={workspaceSlug}
|
||||
workspaceId={workspaceId}
|
||||
projectId={projectId}
|
||||
dragDropEnabled={false}
|
||||
onChange={(_description: object, description_html: string) => {
|
||||
handleData("description_html", description_html);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
@ -0,0 +1,186 @@
|
||||
import { FC, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { LayoutPanelTop } from "lucide-react";
|
||||
import { ISearchIssueResponse, TIssue } from "@plane/types";
|
||||
// components
|
||||
import {
|
||||
CycleDropdown,
|
||||
DateDropdown,
|
||||
EstimateDropdown,
|
||||
ModuleDropdown,
|
||||
PriorityDropdown,
|
||||
MemberDropdown,
|
||||
StateDropdown,
|
||||
} from "@/components/dropdowns";
|
||||
import { ParentIssuesListModal } from "@/components/issues";
|
||||
import { IssueLabelSelect } from "@/components/issues/select";
|
||||
// helpers
|
||||
import { renderFormattedPayloadDate, getDate } from "@/helpers/date-time.helper";
|
||||
// hooks
|
||||
import { useEstimate } from "@/hooks/store";
|
||||
|
||||
type TInboxIssueProperties = {
|
||||
projectId: string;
|
||||
data: Partial<TIssue>;
|
||||
handleData: (issueKey: keyof Partial<TIssue>, issueValue: Partial<TIssue>[keyof Partial<TIssue>]) => void;
|
||||
isVisible?: boolean;
|
||||
};
|
||||
|
||||
export const InboxIssueProperties: FC<TInboxIssueProperties> = observer((props) => {
|
||||
const { projectId, data, handleData, isVisible = false } = props;
|
||||
// hooks
|
||||
const { areEstimatesEnabledForProject } = useEstimate();
|
||||
// states
|
||||
const [parentIssueModalOpen, setParentIssueModalOpen] = useState(false);
|
||||
const [selectedParentIssue, setSelectedParentIssue] = useState<ISearchIssueResponse | undefined>(undefined);
|
||||
true;
|
||||
|
||||
const startDate = data?.start_date;
|
||||
const targetDate = data?.target_date;
|
||||
|
||||
const minDate = getDate(startDate);
|
||||
minDate?.setDate(minDate.getDate());
|
||||
|
||||
const maxDate = getDate(targetDate);
|
||||
maxDate?.setDate(maxDate.getDate());
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-wrap gap-2 items-center">
|
||||
{/* state */}
|
||||
<div className="h-7">
|
||||
<StateDropdown
|
||||
value={data?.state_id || ""}
|
||||
onChange={(stateId) => handleData("state_id", stateId)}
|
||||
projectId={projectId}
|
||||
buttonVariant="border-with-text"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* priority */}
|
||||
<div className="h-7">
|
||||
<PriorityDropdown
|
||||
value={data?.priority || "none"}
|
||||
onChange={(priority) => handleData("priority", priority)}
|
||||
buttonVariant="border-with-text"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Assignees */}
|
||||
<div className="h-7">
|
||||
<MemberDropdown
|
||||
projectId={projectId}
|
||||
value={data?.assignee_ids || []}
|
||||
onChange={(assigneeIds) => handleData("assignee_ids", assigneeIds)}
|
||||
buttonVariant={(data?.assignee_ids || [])?.length > 0 ? "transparent-without-text" : "border-with-text"}
|
||||
buttonClassName={(data?.assignee_ids || [])?.length > 0 ? "hover:bg-transparent" : ""}
|
||||
placeholder="Assignees"
|
||||
multiple
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* labels */}
|
||||
<div className="h-7">
|
||||
<IssueLabelSelect
|
||||
createLabelEnabled={false}
|
||||
setIsOpen={() => {}}
|
||||
value={data?.label_ids || []}
|
||||
onChange={(labelIds) => handleData("label_ids", labelIds)}
|
||||
projectId={projectId}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* start date */}
|
||||
{isVisible && (
|
||||
<div className="h-7">
|
||||
<DateDropdown
|
||||
value={data?.start_date || null}
|
||||
onChange={(date) => (date ? handleData("start_date", renderFormattedPayloadDate(date)) : null)}
|
||||
buttonVariant="border-with-text"
|
||||
minDate={minDate ?? undefined}
|
||||
placeholder="Start date"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* due date */}
|
||||
<div className="h-7">
|
||||
<DateDropdown
|
||||
value={data?.target_date || null}
|
||||
onChange={(date) => (date ? handleData("target_date", renderFormattedPayloadDate(date)) : null)}
|
||||
buttonVariant="border-with-text"
|
||||
minDate={minDate ?? undefined}
|
||||
placeholder="Due date"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* cycle */}
|
||||
{isVisible && (
|
||||
<div className="h-7">
|
||||
<CycleDropdown
|
||||
value={data?.cycle_id || ""}
|
||||
onChange={(cycleId) => handleData("cycle_id", cycleId)}
|
||||
projectId={projectId}
|
||||
placeholder="Cycle"
|
||||
buttonVariant="border-with-text"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* module */}
|
||||
{isVisible && (
|
||||
<div className="h-7">
|
||||
<ModuleDropdown
|
||||
value={data?.module_ids || []}
|
||||
onChange={(moduleIds) => handleData("module_ids", moduleIds)}
|
||||
projectId={projectId}
|
||||
placeholder="Modules"
|
||||
buttonVariant="border-with-text"
|
||||
multiple
|
||||
showCount
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* estimate */}
|
||||
{isVisible && areEstimatesEnabledForProject(projectId) && (
|
||||
<div className="h-7">
|
||||
<EstimateDropdown
|
||||
value={data?.estimate_point || null}
|
||||
onChange={(estimatePoint) => handleData("estimate_point", estimatePoint)}
|
||||
projectId={projectId}
|
||||
buttonVariant="border-with-text"
|
||||
placeholder="Estimate"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* add parent */}
|
||||
{isVisible && (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="flex cursor-pointer items-center justify-between gap-1 rounded border-[0.5px] border-custom-border-300 px-2 py-1.5 text-xs hover:bg-custom-background-80"
|
||||
onClick={() => setParentIssueModalOpen(true)}
|
||||
>
|
||||
<LayoutPanelTop className="h-3 w-3 flex-shrink-0" />
|
||||
<span className="whitespace-nowrap">
|
||||
{selectedParentIssue
|
||||
? `${selectedParentIssue.project__identifier}-${selectedParentIssue.sequence_id}`
|
||||
: `Add parent`}
|
||||
</span>
|
||||
</button>
|
||||
<ParentIssuesListModal
|
||||
isOpen={parentIssueModalOpen}
|
||||
handleClose={() => setParentIssueModalOpen(false)}
|
||||
onChange={(issue) => {
|
||||
handleData("parent_id", issue?.id);
|
||||
setSelectedParentIssue(issue);
|
||||
}}
|
||||
projectId={projectId}
|
||||
issueId={data?.id}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
@ -0,0 +1,27 @@
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { TIssue } from "@plane/types";
|
||||
import { Input } from "@plane/ui";
|
||||
|
||||
type TInboxIssueTitle = {
|
||||
data: Partial<TIssue>;
|
||||
handleData: (issueKey: keyof Partial<TIssue>, issueValue: Partial<TIssue>[keyof Partial<TIssue>]) => void;
|
||||
};
|
||||
|
||||
export const InboxIssueTitle: FC<TInboxIssueTitle> = observer((props) => {
|
||||
const { data, handleData } = props;
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-wrap gap-2 items-center">
|
||||
<Input
|
||||
id="name"
|
||||
name="name"
|
||||
type="text"
|
||||
value={data?.name}
|
||||
onChange={(e) => handleData("name", e.target.value)}
|
||||
placeholder="Title"
|
||||
className="w-full resize-none text-xl"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
84
web/components/inbox/modals/create-edit-modal/modal.tsx
Normal file
84
web/components/inbox/modals/create-edit-modal/modal.tsx
Normal file
@ -0,0 +1,84 @@
|
||||
import { FC, Fragment } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Transition, Dialog } from "@headlessui/react";
|
||||
import { TIssue } from "@plane/types";
|
||||
// components
|
||||
import { InboxIssueCreateRoot, InboxIssueEditRoot } from "@/components/inbox/modals/create-edit-modal";
|
||||
// hooks
|
||||
import { useProject } from "@/hooks/store";
|
||||
|
||||
type TInboxIssueCreateEditModalRoot = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
modalState: boolean;
|
||||
handleModalClose: () => void;
|
||||
issue: Partial<TIssue> | undefined;
|
||||
onSubmit?: () => void;
|
||||
};
|
||||
|
||||
export const InboxIssueCreateEditModalRoot: FC<TInboxIssueCreateEditModalRoot> = observer((props) => {
|
||||
const { workspaceSlug, projectId, modalState, handleModalClose, issue, onSubmit } = props;
|
||||
// hooks
|
||||
const { currentProjectDetails } = useProject();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Transition.Root show={modalState} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-20" onClose={handleModalClose}>
|
||||
<Transition.Child
|
||||
as={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 bg-custom-backdrop transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||||
<div className="my-10 flex items-center justify-center p-4 text-center sm:p-0 md:my-20">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative transform rounded-lg bg-custom-background-100 p-5 text-left shadow-custom-shadow-md transition-all w-full lg:max-w-4xl">
|
||||
{issue && issue?.id ? (
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-xl font-medium text-custom-text-100">
|
||||
Move {currentProjectDetails?.identifier}-{issue?.sequence_id} to project issues
|
||||
</h3>
|
||||
<InboxIssueEditRoot
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issue.id}
|
||||
issue={issue}
|
||||
handleModalClose={handleModalClose}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-xl font-medium text-custom-text-100">Create Inbox Issue</h3>
|
||||
<InboxIssueCreateRoot
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
handleModalClose={handleModalClose}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
</div>
|
||||
);
|
||||
});
|
@ -1,5 +1,5 @@
|
||||
export * from "./accept-issue-modal";
|
||||
export * from "./create-issue-modal";
|
||||
export * from "./create-edit-modal";
|
||||
export * from "./decline-issue-modal";
|
||||
export * from "./delete-issue-modal";
|
||||
export * from "./select-duplicate";
|
||||
|
@ -21,7 +21,7 @@ type TInboxIssueRoot = {
|
||||
export const InboxIssueRoot: FC<TInboxIssueRoot> = observer((props) => {
|
||||
const { workspaceSlug, projectId, inboxIssueId, inboxAccessible } = props;
|
||||
// hooks
|
||||
const { isLoading, error, fetchInboxIssues } = useProjectInbox();
|
||||
const { loader, error, fetchInboxIssues } = useProjectInbox();
|
||||
|
||||
useSWR(
|
||||
inboxAccessible && workspaceSlug && projectId ? `PROJECT_INBOX_ISSUES_${workspaceSlug}_${projectId}` : null,
|
||||
@ -35,7 +35,7 @@ export const InboxIssueRoot: FC<TInboxIssueRoot> = observer((props) => {
|
||||
);
|
||||
|
||||
// loader
|
||||
if (isLoading === "init-loading")
|
||||
if (loader === "init-loading")
|
||||
return (
|
||||
<div className="relative flex w-full h-full flex-col">
|
||||
<InboxLayoutLoader />
|
||||
|
@ -42,7 +42,7 @@ export const InboxSidebar: FC<IInboxSidebarProps> = observer((props) => {
|
||||
const {
|
||||
currentTab,
|
||||
handleCurrentTab,
|
||||
isLoading,
|
||||
loader,
|
||||
inboxIssuesArray,
|
||||
inboxIssuePaginationInfo,
|
||||
fetchInboxPaginationIssues,
|
||||
@ -100,7 +100,7 @@ export const InboxSidebar: FC<IInboxSidebarProps> = observer((props) => {
|
||||
|
||||
<InboxIssueAppliedFilters />
|
||||
|
||||
{isLoading != undefined && isLoading === "filter-loading" && !inboxIssuePaginationInfo?.next_page_results ? (
|
||||
{loader != undefined && loader === "filter-loading" && !inboxIssuePaginationInfo?.next_page_results ? (
|
||||
<InboxSidebarLoader />
|
||||
) : (
|
||||
<div
|
||||
|
@ -649,6 +649,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
projectId={projectId}
|
||||
buttonVariant="border-with-text"
|
||||
tabIndex={getTabIndex("estimate_point")}
|
||||
placeholder="Estimate"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
@ -20,10 +20,11 @@ type Props = {
|
||||
label?: JSX.Element;
|
||||
disabled?: boolean;
|
||||
tabIndex?: number;
|
||||
createLabelEnabled?: boolean;
|
||||
};
|
||||
|
||||
export const IssueLabelSelect: React.FC<Props> = observer((props) => {
|
||||
const { setIsOpen, value, onChange, projectId, label, disabled = false, tabIndex } = props;
|
||||
const { setIsOpen, value, onChange, projectId, label, disabled = false, tabIndex, createLabelEnabled = true } = props;
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
@ -221,14 +222,16 @@ export const IssueLabelSelect: React.FC<Props> = observer((props) => {
|
||||
) : (
|
||||
<p className="text-custom-text-400 italic py-1 px-1.5">Loading...</p>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-2 w-full select-none rounded px-1 py-2 hover:bg-custom-background-80"
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
<Plus className="h-3 w-3" aria-hidden="true" />
|
||||
<span className="whitespace-nowrap">Create new label</span>
|
||||
</button>
|
||||
{createLabelEnabled && (
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-2 w-full select-none rounded px-1 py-2 hover:bg-custom-background-80"
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
<Plus className="h-3 w-3" aria-hidden="true" />
|
||||
<span className="whitespace-nowrap">Create new label</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Combobox.Options>
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { useContext } from "react";
|
||||
// mobx store
|
||||
import { StoreContext } from "contexts/store-context";
|
||||
import { IInboxIssueStore } from "@/store/inbox/inbox-issue.store";
|
||||
|
||||
export const useInboxIssues = (inboxIssueId: string) => {
|
||||
export const useInboxIssues = (inboxIssueId: string): IInboxIssueStore => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useInboxIssues must be used within StoreProvider");
|
||||
return context.projectInbox.getIssueInboxByIssueId(inboxIssueId) || {};
|
||||
return context.projectInbox.getIssueInboxByIssueId(inboxIssueId);
|
||||
};
|
||||
|
@ -37,7 +37,7 @@
|
||||
"dotenv": "^16.0.3",
|
||||
"js-cookie": "^3.0.1",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.294.0",
|
||||
"lucide-react": "^0.368.0",
|
||||
"mobx": "^6.10.0",
|
||||
"mobx-react": "^9.1.0",
|
||||
"mobx-utils": "^6.0.8",
|
||||
|
@ -1,3 +1,4 @@
|
||||
import clone from "lodash/clone";
|
||||
import set from "lodash/set";
|
||||
import { makeObservable, observable, runInAction, action } from "mobx";
|
||||
import { TIssue, TInboxIssue, TInboxIssueStatus, TInboxDuplicateIssueDetails } from "@plane/types";
|
||||
@ -5,6 +6,7 @@ import { TIssue, TInboxIssue, TInboxIssueStatus, TInboxDuplicateIssueDetails } f
|
||||
import { EInboxIssueStatus } from "@/helpers/inbox.helper";
|
||||
// services
|
||||
import { InboxIssueService } from "@/services/inbox";
|
||||
import { IssueService } from "@/services/issue";
|
||||
// root store
|
||||
import { RootStore } from "@/store/root.store";
|
||||
|
||||
@ -22,6 +24,8 @@ export interface IInboxIssueStore {
|
||||
updateInboxIssueDuplicateTo: (issueId: string) => Promise<void>; // connecting the inbox issue to the project existing issue
|
||||
updateInboxIssueSnoozeTill: (date: Date) => Promise<void>; // snooze the issue
|
||||
updateIssue: (issue: Partial<TIssue>) => Promise<void>; // updating the issue
|
||||
updateProjectIssue: (issue: Partial<TIssue>) => Promise<void>; // updating the issue
|
||||
fetchIssueActivity: () => Promise<void>; // fetching the issue activity
|
||||
}
|
||||
|
||||
export class InboxIssueStore implements IInboxIssueStore {
|
||||
@ -38,6 +42,7 @@ export class InboxIssueStore implements IInboxIssueStore {
|
||||
projectId: string;
|
||||
// services
|
||||
inboxIssueService;
|
||||
issueService;
|
||||
|
||||
constructor(workspaceSlug: string, projectId: string, data: TInboxIssue, private store: RootStore) {
|
||||
this.id = data.id;
|
||||
@ -51,6 +56,7 @@ export class InboxIssueStore implements IInboxIssueStore {
|
||||
this.projectId = projectId;
|
||||
// services
|
||||
this.inboxIssueService = new InboxIssueService();
|
||||
this.issueService = new IssueService();
|
||||
// observable variables should be defined after the initialization of the values
|
||||
makeObservable(this, {
|
||||
id: observable,
|
||||
@ -65,6 +71,8 @@ export class InboxIssueStore implements IInboxIssueStore {
|
||||
updateInboxIssueDuplicateTo: action,
|
||||
updateInboxIssueSnoozeTill: action,
|
||||
updateIssue: action,
|
||||
updateProjectIssue: action,
|
||||
fetchIssueActivity: action,
|
||||
});
|
||||
}
|
||||
|
||||
@ -72,12 +80,13 @@ export class InboxIssueStore implements IInboxIssueStore {
|
||||
const previousData: Partial<TInboxIssue> = {
|
||||
status: this.status,
|
||||
};
|
||||
|
||||
try {
|
||||
if (!this.issue.id) return;
|
||||
set(this, "status", status);
|
||||
await this.inboxIssueService.update(this.workspaceSlug, this.projectId, this.issue.id, {
|
||||
const inboxIssue = await this.inboxIssueService.update(this.workspaceSlug, this.projectId, this.issue.id, {
|
||||
status: status,
|
||||
});
|
||||
runInAction(() => set(this, "status", inboxIssue?.status));
|
||||
} catch {
|
||||
runInAction(() => set(this, "status", previousData.status));
|
||||
}
|
||||
@ -85,20 +94,19 @@ export class InboxIssueStore implements IInboxIssueStore {
|
||||
|
||||
updateInboxIssueDuplicateTo = async (issueId: string) => {
|
||||
const inboxStatus = EInboxIssueStatus.DUPLICATE;
|
||||
|
||||
const previousData: Partial<TInboxIssue> = {
|
||||
status: this.status,
|
||||
duplicate_to: this.duplicate_to,
|
||||
};
|
||||
|
||||
try {
|
||||
if (!this.issue.id) return;
|
||||
set(this, "status", inboxStatus);
|
||||
set(this, "duplicate_to", issueId);
|
||||
const issueResponse = await this.inboxIssueService.update(this.workspaceSlug, this.projectId, this.issue.id, {
|
||||
status: inboxStatus,
|
||||
duplicate_to: issueId,
|
||||
});
|
||||
runInAction(() => {
|
||||
this.status = issueResponse.status;
|
||||
this.duplicate_to = issueResponse.duplicate_to;
|
||||
this.duplicate_issue_detail = issueResponse.duplicate_issue_detail;
|
||||
});
|
||||
@ -112,19 +120,21 @@ export class InboxIssueStore implements IInboxIssueStore {
|
||||
|
||||
updateInboxIssueSnoozeTill = async (date: Date) => {
|
||||
const inboxStatus = EInboxIssueStatus.SNOOZED;
|
||||
|
||||
const previousData: Partial<TInboxIssue> = {
|
||||
status: this.status,
|
||||
snoozed_till: this.snoozed_till,
|
||||
};
|
||||
|
||||
try {
|
||||
if (!this.issue.id) return;
|
||||
set(this, "status", inboxStatus);
|
||||
set(this, "snoozed_till", date);
|
||||
await this.inboxIssueService.update(this.workspaceSlug, this.projectId, this.issue.id, {
|
||||
const issueResponse = await this.inboxIssueService.update(this.workspaceSlug, this.projectId, this.issue.id, {
|
||||
status: inboxStatus,
|
||||
snoozed_till: new Date(date),
|
||||
});
|
||||
runInAction(() => {
|
||||
this.status = issueResponse?.status;
|
||||
this.snoozed_till = issueResponse?.snoozed_till ? new Date(issueResponse.snoozed_till) : undefined;
|
||||
});
|
||||
} catch {
|
||||
runInAction(() => {
|
||||
set(this, "status", previousData.status);
|
||||
@ -134,21 +144,49 @@ export class InboxIssueStore implements IInboxIssueStore {
|
||||
};
|
||||
|
||||
updateIssue = async (issue: Partial<TIssue>) => {
|
||||
const inboxIssue = this.issue;
|
||||
const inboxIssue = clone(this.issue);
|
||||
try {
|
||||
if (!this.issue.id) return;
|
||||
Object.keys(issue).forEach((key) => {
|
||||
const issueKey = key as keyof TIssue;
|
||||
set(inboxIssue, issueKey, issue[issueKey]);
|
||||
set(this.issue, issueKey, issue[issueKey]);
|
||||
});
|
||||
await this.inboxIssueService.updateIssue(this.workspaceSlug, this.projectId, this.issue.id, issue);
|
||||
// fetching activity
|
||||
await this.store.issue.issueDetail.fetchActivities(this.workspaceSlug, this.projectId, this.issue.id);
|
||||
this.fetchIssueActivity();
|
||||
} catch {
|
||||
Object.keys(issue).forEach((key) => {
|
||||
const issueKey = key as keyof TIssue;
|
||||
set(inboxIssue, issueKey, inboxIssue[issueKey]);
|
||||
set(this.issue, issueKey, inboxIssue[issueKey]);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
updateProjectIssue = async (issue: Partial<TIssue>) => {
|
||||
const inboxIssue = clone(this.issue);
|
||||
try {
|
||||
if (!this.issue.id) return;
|
||||
Object.keys(issue).forEach((key) => {
|
||||
const issueKey = key as keyof TIssue;
|
||||
set(this.issue, issueKey, issue[issueKey]);
|
||||
});
|
||||
await this.issueService.patchIssue(this.workspaceSlug, this.projectId, this.issue.id, issue);
|
||||
// fetching activity
|
||||
this.fetchIssueActivity();
|
||||
} catch {
|
||||
Object.keys(issue).forEach((key) => {
|
||||
const issueKey = key as keyof TIssue;
|
||||
set(this.issue, issueKey, inboxIssue[issueKey]);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
fetchIssueActivity = async () => {
|
||||
try {
|
||||
if (!this.issue.id) return;
|
||||
await this.store.issue.issueDetail.fetchActivities(this.workspaceSlug, this.projectId, this.issue.id);
|
||||
} catch {
|
||||
console.error("Failed to fetch issue activity");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ type TLoader =
|
||||
|
||||
export interface IProjectInboxStore {
|
||||
currentTab: TInboxIssueCurrentTab;
|
||||
isLoading: TLoader;
|
||||
loader: TLoader;
|
||||
error: { message: string; status: "init-error" | "pagination-error" } | undefined;
|
||||
currentInboxProjectId: string;
|
||||
inboxFilters: Partial<TInboxIssueFilter>;
|
||||
@ -42,7 +42,7 @@ export interface IProjectInboxStore {
|
||||
getAppliedFiltersCount: number;
|
||||
inboxIssuesArray: IInboxIssueStore[];
|
||||
// helper actions
|
||||
getIssueInboxByIssueId: (issueId: string) => IInboxIssueStore | undefined;
|
||||
getIssueInboxByIssueId: (issueId: string) => IInboxIssueStore;
|
||||
inboxIssueSorting: (issues: IInboxIssueStore[]) => IInboxIssueStore[];
|
||||
inboxIssueQueryParams: (
|
||||
inboxFilters: Partial<TInboxIssueFilter>,
|
||||
@ -70,7 +70,7 @@ export class ProjectInboxStore implements IProjectInboxStore {
|
||||
PER_PAGE_COUNT = 10;
|
||||
// observables
|
||||
currentTab: TInboxIssueCurrentTab = EInboxIssueCurrentTab.OPEN;
|
||||
isLoading: TLoader = "init-loading";
|
||||
loader: TLoader = "init-loading";
|
||||
error: { message: string; status: "init-error" | "pagination-error" } | undefined = undefined;
|
||||
currentInboxProjectId: string = "";
|
||||
inboxFilters: Partial<TInboxIssueFilter> = {
|
||||
@ -88,7 +88,7 @@ export class ProjectInboxStore implements IProjectInboxStore {
|
||||
constructor(private store: RootStore) {
|
||||
makeObservable(this, {
|
||||
currentTab: observable.ref,
|
||||
isLoading: observable.ref,
|
||||
loader: observable.ref,
|
||||
error: observable,
|
||||
currentInboxProjectId: observable.ref,
|
||||
inboxFilters: observable,
|
||||
@ -123,17 +123,17 @@ export class ProjectInboxStore implements IProjectInboxStore {
|
||||
}
|
||||
|
||||
get inboxIssuesArray() {
|
||||
let appliedFilters =
|
||||
this.currentTab === EInboxIssueCurrentTab.OPEN
|
||||
? [EInboxIssueStatus.PENDING, EInboxIssueStatus.SNOOZED]
|
||||
: [EInboxIssueStatus.ACCEPTED, EInboxIssueStatus.DECLINED, EInboxIssueStatus.DUPLICATE];
|
||||
appliedFilters = appliedFilters.filter((filter) => this.inboxFilters?.status?.includes(filter));
|
||||
return this.inboxIssueSorting(
|
||||
Object.values(this.inboxIssues || {}).filter((inbox) =>
|
||||
(this.currentTab === EInboxIssueCurrentTab.OPEN
|
||||
? [EInboxIssueStatus.PENDING, EInboxIssueStatus.SNOOZED]
|
||||
: [EInboxIssueStatus.ACCEPTED, EInboxIssueStatus.DECLINED, EInboxIssueStatus.DUPLICATE]
|
||||
).includes(inbox.status)
|
||||
)
|
||||
Object.values(this.inboxIssues || {}).filter((inbox) => appliedFilters.includes(inbox.status))
|
||||
);
|
||||
}
|
||||
|
||||
getIssueInboxByIssueId = computedFn((issueId: string) => this.inboxIssues?.[issueId] || undefined);
|
||||
getIssueInboxByIssueId = computedFn((issueId: string) => this.inboxIssues?.[issueId]);
|
||||
|
||||
// helpers
|
||||
inboxIssueSorting = (issues: IInboxIssueStore[]) => {
|
||||
@ -252,9 +252,9 @@ export class ProjectInboxStore implements IProjectInboxStore {
|
||||
set(this, ["inboxIssues"], {});
|
||||
set(this, ["inboxIssuePaginationInfo"], undefined);
|
||||
}
|
||||
if (Object.keys(this.inboxIssues).length === 0) this.isLoading = "init-loading";
|
||||
else this.isLoading = "mutation-loading";
|
||||
if (loadingType) this.isLoading = loadingType;
|
||||
if (Object.keys(this.inboxIssues).length === 0) this.loader = "init-loading";
|
||||
else this.loader = "mutation-loading";
|
||||
if (loadingType) this.loader = loadingType;
|
||||
|
||||
const queryParams = this.inboxIssueQueryParams(
|
||||
this.inboxFilters,
|
||||
@ -265,7 +265,7 @@ export class ProjectInboxStore implements IProjectInboxStore {
|
||||
const { results, ...paginationInfo } = await this.inboxIssueService.list(workspaceSlug, projectId, queryParams);
|
||||
|
||||
runInAction(() => {
|
||||
this.isLoading = undefined;
|
||||
this.loader = undefined;
|
||||
set(this, "inboxIssuePaginationInfo", paginationInfo);
|
||||
if (results && results.length > 0)
|
||||
results.forEach((value: TInboxIssue) => {
|
||||
@ -279,7 +279,7 @@ export class ProjectInboxStore implements IProjectInboxStore {
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error fetching the inbox issues", error);
|
||||
this.isLoading = undefined;
|
||||
this.loader = undefined;
|
||||
this.error = {
|
||||
message: "Error fetching the inbox issues please try again later.",
|
||||
status: "init-error",
|
||||
@ -301,7 +301,7 @@ export class ProjectInboxStore implements IProjectInboxStore {
|
||||
(this.inboxIssuePaginationInfo?.total_results &&
|
||||
this.inboxIssuesArray.length < this.inboxIssuePaginationInfo?.total_results))
|
||||
) {
|
||||
this.isLoading = "pagination-loading";
|
||||
this.loader = "pagination-loading";
|
||||
|
||||
const queryParams = this.inboxIssueQueryParams(
|
||||
this.inboxFilters,
|
||||
@ -312,7 +312,7 @@ export class ProjectInboxStore implements IProjectInboxStore {
|
||||
const { results, ...paginationInfo } = await this.inboxIssueService.list(workspaceSlug, projectId, queryParams);
|
||||
|
||||
runInAction(() => {
|
||||
this.isLoading = undefined;
|
||||
this.loader = undefined;
|
||||
set(this, "inboxIssuePaginationInfo", paginationInfo);
|
||||
if (results && results.length > 0)
|
||||
results.forEach((value: TInboxIssue) => {
|
||||
@ -327,7 +327,7 @@ export class ProjectInboxStore implements IProjectInboxStore {
|
||||
} else set(this, ["inboxIssuePaginationInfo", "next_page_results"], false);
|
||||
} catch (error) {
|
||||
console.error("Error fetching the inbox issues", error);
|
||||
this.isLoading = undefined;
|
||||
this.loader = undefined;
|
||||
this.error = {
|
||||
message: "Error fetching the paginated inbox issues please try again later.",
|
||||
status: "pagination-error",
|
||||
@ -348,7 +348,7 @@ export class ProjectInboxStore implements IProjectInboxStore {
|
||||
inboxIssueId: string
|
||||
): Promise<TInboxIssue> => {
|
||||
try {
|
||||
this.isLoading = "issue-loading";
|
||||
this.loader = "issue-loading";
|
||||
const inboxIssue = await this.inboxIssueService.retrieve(workspaceSlug, projectId, inboxIssueId);
|
||||
const issueId = inboxIssue?.issue?.id || undefined;
|
||||
|
||||
@ -362,12 +362,12 @@ export class ProjectInboxStore implements IProjectInboxStore {
|
||||
await this.store.issue.issueDetail.fetchActivities(workspaceSlug, projectId, issueId);
|
||||
// fetching comments
|
||||
await this.store.issue.issueDetail.fetchComments(workspaceSlug, projectId, issueId);
|
||||
this.isLoading = undefined;
|
||||
this.loader = undefined;
|
||||
}
|
||||
return inboxIssue;
|
||||
} catch (error) {
|
||||
console.error("Error fetching the inbox issue with inbox issue id");
|
||||
this.isLoading = undefined;
|
||||
this.loader = undefined;
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
27
yarn.lock
27
yarn.lock
@ -5874,6 +5874,11 @@ lucide-react@^0.309.0:
|
||||
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.309.0.tgz#7369893cb4b074a0a0b1d3acdc6fd9a8bdb5add1"
|
||||
integrity sha512-zNVPczuwFrCfksZH3zbd1UDE6/WYhYAdbe2k7CImVyPAkXLgIwbs6eXQ4loigqDnUFjyFYCI5jZ1y10Kqal0dg==
|
||||
|
||||
lucide-react@^0.368.0:
|
||||
version "0.368.0"
|
||||
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.368.0.tgz#3c0ee63f4f7d30ae63b621b2b8f04f9e409ee6e7"
|
||||
integrity sha512-soryVrCjheZs8rbXKdINw9B8iPi5OajBJZMJ1HORig89ljcOcEokKKAgGbg3QWxSXel7JwHOfDFUdDHAKyUAMw==
|
||||
|
||||
magic-string@^0.25.0, magic-string@^0.25.7:
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c"
|
||||
@ -7815,8 +7820,16 @@ streamx@^2.15.0:
|
||||
fast-fifo "^1.1.0"
|
||||
queue-tick "^1.0.1"
|
||||
|
||||
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0:
|
||||
name string-width-cjs
|
||||
"string-width-cjs@npm:string-width@^4.2.0":
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
dependencies:
|
||||
emoji-regex "^8.0.0"
|
||||
is-fullwidth-code-point "^3.0.0"
|
||||
strip-ansi "^6.0.1"
|
||||
|
||||
string-width@^4.1.0:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
@ -7892,8 +7905,14 @@ stringify-object@^3.3.0:
|
||||
is-obj "^1.0.1"
|
||||
is-regexp "^1.0.0"
|
||||
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
name strip-ansi-cjs
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
dependencies:
|
||||
ansi-regex "^5.0.1"
|
||||
|
||||
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
|
Loading…
Reference in New Issue
Block a user