diff --git a/apiserver/plane/app/views/inbox/base.py b/apiserver/plane/app/views/inbox/base.py index 4536d6a19..304e7dce5 100644 --- a/apiserver/plane/app/views/inbox/base.py +++ b/apiserver/plane/app/views/inbox/base.py @@ -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", "

" - ), - 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( diff --git a/web/components/headers/project-inbox.tsx b/web/components/headers/project-inbox.tsx index b35c7485d..16f193234 100644 --- a/web/components/headers/project-inbox.tsx +++ b/web/components/headers/project-inbox.tsx @@ -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 (
@@ -51,7 +51,7 @@ export const ProjectInboxHeader: FC = observer(() => { /> - {isLoading === "pagination-loading" && ( + {loader === "pagination-loading" && (

Syncing...

@@ -60,9 +60,16 @@ export const ProjectInboxHeader: FC = observer(() => {
- {currentProjectDetails?.inbox_view && ( + {currentProjectDetails?.inbox_view && workspaceSlug && projectId && (
- setCreateIssueModal(false)} /> + setCreateIssueModal(false)} + issue={undefined} + /> + diff --git a/web/components/inbox/content/inbox-issue-header.tsx b/web/components/inbox/content/inbox-issue-header.tsx index f494c2b76..41a56ef83 100644 --- a/web/components/inbox/content/inbox-issue-header.tsx +++ b/web/components/inbox/content/inbox-issue-header.tsx @@ -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 = 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 = 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 = observer((p onSubmit={handleInboxIssueDuplicate} /> - setAcceptIssueModal(false)} + setAcceptIssueModal(false)} + issue={inboxIssue?.issue} onSubmit={handleInboxIssueAccept} /> @@ -156,14 +193,12 @@ export const InboxIssueActionsHeader: FC = observer((p onClose={() => setDeclineIssueModal(false)} onSubmit={handleInboxIssueDecline} /> - setDeleteIssueModal(false)} onSubmit={handleInboxIssueDelete} /> - setIsSnoozeDateModalOpen(false)} @@ -206,7 +241,13 @@ export const InboxIssueActionsHeader: FC = observer((p
{canMarkAsAccepted && (
-
@@ -214,7 +255,13 @@ export const InboxIssueActionsHeader: FC = observer((p {canMarkAsDeclined && (
-
diff --git a/web/components/inbox/content/issue-properties.tsx b/web/components/inbox/content/issue-properties.tsx index 670b7b1b8..2adf4bdc7 100644 --- a/web/components/inbox/content/issue-properties.tsx +++ b/web/components/inbox/content/issue-properties.tsx @@ -21,7 +21,7 @@ type Props = { duplicateIssueDetails: TInboxDuplicateIssueDetails | undefined; }; -export const InboxIssueProperties: React.FC = observer((props) => { +export const InboxIssueContentProperties: React.FC = observer((props) => { const { workspaceSlug, projectId, issue, issueOperations, isEditable, duplicateIssueDetails } = props; const router = useRouter(); diff --git a/web/components/inbox/content/issue-root.tsx b/web/components/inbox/content/issue-root.tsx index 627c86c45..87ca066aa 100644 --- a/web/components/inbox/content/issue-root.tsx +++ b/web/components/inbox/content/issue-root.tsx @@ -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 = 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 = observer((props) => { value={issue.name} /> -

"} - disabled={!isEditable} - issueOperations={issueOperations} - setIsSubmitting={(value) => setIsSubmitting(value)} - /> + {loader === "issue-loading" ? ( + + + + ) : ( +

"} + disabled={!isEditable} + issueOperations={issueOperations} + setIsSubmitting={(value) => setIsSubmitting(value)} + /> + )} {currentUser && ( = observer((props) => { )}
- void; +}; + +export const defaultIssueData: Partial = { + id: undefined, + name: "", + description_html: "", + priority: "none", + state_id: "", + label_ids: [], + assignee_ids: [], + start_date: renderFormattedPayloadDate(new Date()), + target_date: "", +}; + +export const InboxIssueCreateRoot: FC = observer((props) => { + const { workspaceSlug, projectId, handleModalClose } = props; + const router = useRouter(); + // refs + const descriptionEditorRef = useRef(null); + // hooks + const { captureIssueEvent } = useEventTracker(); + const { createInboxIssue } = useProjectInbox(); + const { getWorkspaceBySlug } = useWorkspace(); + const workspaceId = getWorkspaceBySlug(workspaceSlug)?.id; + // states + const [createMore, setCreateMore] = useState(false); + const [formSubmitting, setFormSubmitting] = useState(false); + const [formData, setFormData] = useState>(defaultIssueData); + const handleFormData = useCallback( + >(issueKey: T, issueValue: Partial[T]) => { + setFormData({ + ...formData, + [issueKey]: issueValue, + }); + }, + [formData] + ); + + const handleFormSubmit = async () => { + const payload: Partial = { + name: formData.name || "", + description_html: formData.description_html || "

", + 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 ( +
+ + + +
+
setCreateMore((prevData) => !prevData)}> + Create more + {}} size="md" /> +
+
+ + +
+
+
+ ); +}); diff --git a/web/components/inbox/modals/create-edit-modal/edit-root.tsx b/web/components/inbox/modals/create-edit-modal/edit-root.tsx new file mode 100644 index 000000000..f0e66c503 --- /dev/null +++ b/web/components/inbox/modals/create-edit-modal/edit-root.tsx @@ -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; + handleModalClose: () => void; + onSubmit?: () => void; +}; + +export const InboxIssueEditRoot: FC = observer((props) => { + const { workspaceSlug, projectId, issueId, issue, handleModalClose, onSubmit } = props; + const router = useRouter(); + // refs + const descriptionEditorRef = useRef(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 | undefined>(undefined); + const handleFormData = useCallback( + >(issueKey: T, issueValue: Partial[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 ?? "

", + 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 = { + name: formData?.name || "", + description_html: formData?.description_html || "

", + 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 ( +
+ + + +
+ + +
+
+ ); +}); diff --git a/web/components/inbox/modals/create-edit-modal/index.ts b/web/components/inbox/modals/create-edit-modal/index.ts new file mode 100644 index 000000000..484c1a31e --- /dev/null +++ b/web/components/inbox/modals/create-edit-modal/index.ts @@ -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"; diff --git a/web/components/inbox/modals/create-edit-modal/issue-description.tsx b/web/components/inbox/modals/create-edit-modal/issue-description.tsx new file mode 100644 index 000000000..313b94527 --- /dev/null +++ b/web/components/inbox/modals/create-edit-modal/issue-description.tsx @@ -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; + handleData: (issueKey: keyof Partial, issueValue: Partial[keyof Partial]) => void; + editorRef: RefObject; +}; + +// TODO: have to implement GPT Assistance +export const InboxIssueDescription: FC = observer((props) => { + const { workspaceSlug, projectId, workspaceId, data, handleData, editorRef } = props; + // hooks + const { loader } = useProjectInbox(); + + if (loader === "issue-loading") + return ( + + + + ); + return ( +
+

" : data?.description_html} + ref={editorRef} + workspaceSlug={workspaceSlug} + workspaceId={workspaceId} + projectId={projectId} + dragDropEnabled={false} + onChange={(_description: object, description_html: string) => { + handleData("description_html", description_html); + }} + /> +
+ ); +}); diff --git a/web/components/inbox/modals/create-edit-modal/issue-properties.tsx b/web/components/inbox/modals/create-edit-modal/issue-properties.tsx new file mode 100644 index 000000000..8e2eda1cc --- /dev/null +++ b/web/components/inbox/modals/create-edit-modal/issue-properties.tsx @@ -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; + handleData: (issueKey: keyof Partial, issueValue: Partial[keyof Partial]) => void; + isVisible?: boolean; +}; + +export const InboxIssueProperties: FC = observer((props) => { + const { projectId, data, handleData, isVisible = false } = props; + // hooks + const { areEstimatesEnabledForProject } = useEstimate(); + // states + const [parentIssueModalOpen, setParentIssueModalOpen] = useState(false); + const [selectedParentIssue, setSelectedParentIssue] = useState(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 ( +
+ {/* state */} +
+ handleData("state_id", stateId)} + projectId={projectId} + buttonVariant="border-with-text" + /> +
+ + {/* priority */} +
+ handleData("priority", priority)} + buttonVariant="border-with-text" + /> +
+ + {/* Assignees */} +
+ 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 + /> +
+ + {/* labels */} +
+ {}} + value={data?.label_ids || []} + onChange={(labelIds) => handleData("label_ids", labelIds)} + projectId={projectId} + /> +
+ + {/* start date */} + {isVisible && ( +
+ (date ? handleData("start_date", renderFormattedPayloadDate(date)) : null)} + buttonVariant="border-with-text" + minDate={minDate ?? undefined} + placeholder="Start date" + /> +
+ )} + + {/* due date */} +
+ (date ? handleData("target_date", renderFormattedPayloadDate(date)) : null)} + buttonVariant="border-with-text" + minDate={minDate ?? undefined} + placeholder="Due date" + /> +
+ + {/* cycle */} + {isVisible && ( +
+ handleData("cycle_id", cycleId)} + projectId={projectId} + placeholder="Cycle" + buttonVariant="border-with-text" + /> +
+ )} + + {/* module */} + {isVisible && ( +
+ handleData("module_ids", moduleIds)} + projectId={projectId} + placeholder="Modules" + buttonVariant="border-with-text" + multiple + showCount + /> +
+ )} + + {/* estimate */} + {isVisible && areEstimatesEnabledForProject(projectId) && ( +
+ handleData("estimate_point", estimatePoint)} + projectId={projectId} + buttonVariant="border-with-text" + placeholder="Estimate" + /> +
+ )} + + {/* add parent */} + {isVisible && ( + <> + + setParentIssueModalOpen(false)} + onChange={(issue) => { + handleData("parent_id", issue?.id); + setSelectedParentIssue(issue); + }} + projectId={projectId} + issueId={data?.id} + /> + + )} +
+ ); +}); diff --git a/web/components/inbox/modals/create-edit-modal/issue-title.tsx b/web/components/inbox/modals/create-edit-modal/issue-title.tsx new file mode 100644 index 000000000..8b1df70c6 --- /dev/null +++ b/web/components/inbox/modals/create-edit-modal/issue-title.tsx @@ -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; + handleData: (issueKey: keyof Partial, issueValue: Partial[keyof Partial]) => void; +}; + +export const InboxIssueTitle: FC = observer((props) => { + const { data, handleData } = props; + + return ( +
+ handleData("name", e.target.value)} + placeholder="Title" + className="w-full resize-none text-xl" + /> +
+ ); +}); diff --git a/web/components/inbox/modals/create-edit-modal/modal.tsx b/web/components/inbox/modals/create-edit-modal/modal.tsx new file mode 100644 index 000000000..554af0948 --- /dev/null +++ b/web/components/inbox/modals/create-edit-modal/modal.tsx @@ -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 | undefined; + onSubmit?: () => void; +}; + +export const InboxIssueCreateEditModalRoot: FC = observer((props) => { + const { workspaceSlug, projectId, modalState, handleModalClose, issue, onSubmit } = props; + // hooks + const { currentProjectDetails } = useProject(); + + return ( +
+ + + +
+ + +
+
+ + + {issue && issue?.id ? ( +
+

+ Move {currentProjectDetails?.identifier}-{issue?.sequence_id} to project issues +

+ +
+ ) : ( +
+

Create Inbox Issue

+ +
+ )} +
+
+
+
+
+
+
+ ); +}); diff --git a/web/components/inbox/modals/index.ts b/web/components/inbox/modals/index.ts index 7ac26685b..fd46f638e 100644 --- a/web/components/inbox/modals/index.ts +++ b/web/components/inbox/modals/index.ts @@ -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"; diff --git a/web/components/inbox/root.tsx b/web/components/inbox/root.tsx index 29a59308d..258421708 100644 --- a/web/components/inbox/root.tsx +++ b/web/components/inbox/root.tsx @@ -21,7 +21,7 @@ type TInboxIssueRoot = { export const InboxIssueRoot: FC = 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 = observer((props) => { ); // loader - if (isLoading === "init-loading") + if (loader === "init-loading") return (
diff --git a/web/components/inbox/sidebar/root.tsx b/web/components/inbox/sidebar/root.tsx index cd8e0c973..047898eba 100644 --- a/web/components/inbox/sidebar/root.tsx +++ b/web/components/inbox/sidebar/root.tsx @@ -42,7 +42,7 @@ export const InboxSidebar: FC = observer((props) => { const { currentTab, handleCurrentTab, - isLoading, + loader, inboxIssuesArray, inboxIssuePaginationInfo, fetchInboxPaginationIssues, @@ -100,7 +100,7 @@ export const InboxSidebar: FC = observer((props) => { - {isLoading != undefined && isLoading === "filter-loading" && !inboxIssuePaginationInfo?.next_page_results ? ( + {loader != undefined && loader === "filter-loading" && !inboxIssuePaginationInfo?.next_page_results ? ( ) : (
= observer((props) => { projectId={projectId} buttonVariant="border-with-text" tabIndex={getTabIndex("estimate_point")} + placeholder="Estimate" />
)} diff --git a/web/components/issues/select/label.tsx b/web/components/issues/select/label.tsx index 32bcc4160..f487b50c1 100644 --- a/web/components/issues/select/label.tsx +++ b/web/components/issues/select/label.tsx @@ -20,10 +20,11 @@ type Props = { label?: JSX.Element; disabled?: boolean; tabIndex?: number; + createLabelEnabled?: boolean; }; export const IssueLabelSelect: React.FC = 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 = observer((props) => { ) : (

Loading...

)} - + {createLabelEnabled && ( + + )}
diff --git a/web/hooks/store/use-inbox-issues.ts b/web/hooks/store/use-inbox-issues.ts index f1afe751d..64c23cea3 100644 --- a/web/hooks/store/use-inbox-issues.ts +++ b/web/hooks/store/use-inbox-issues.ts @@ -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); }; diff --git a/web/package.json b/web/package.json index de73ad407..bac12fb65 100644 --- a/web/package.json +++ b/web/package.json @@ -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", diff --git a/web/store/inbox/inbox-issue.store.ts b/web/store/inbox/inbox-issue.store.ts index 3f8689d6b..4afa25bbd 100644 --- a/web/store/inbox/inbox-issue.store.ts +++ b/web/store/inbox/inbox-issue.store.ts @@ -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; // connecting the inbox issue to the project existing issue updateInboxIssueSnoozeTill: (date: Date) => Promise; // snooze the issue updateIssue: (issue: Partial) => Promise; // updating the issue + updateProjectIssue: (issue: Partial) => Promise; // updating the issue + fetchIssueActivity: () => Promise; // 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 = { 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 = { 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 = { 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) => { - 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) => { + 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"); + } + }; } diff --git a/web/store/inbox/project-inbox.store.ts b/web/store/inbox/project-inbox.store.ts index b253b76fc..59ba84ef4 100644 --- a/web/store/inbox/project-inbox.store.ts +++ b/web/store/inbox/project-inbox.store.ts @@ -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; @@ -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, @@ -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 = { @@ -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 => { 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; } }; diff --git a/yarn.lock b/yarn.lock index d8cca2a77..13d932a41 100644 --- a/yarn.lock +++ b/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==